Compare commits

..

27 Commits

Author SHA1 Message Date
a17d2c9d66 Major refactoring: HTTP client unification and project restructuring
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 13m53s
## HTTP Client Refactoring
- Create unified HttpClient module (src/http_client.rs)
- Refactor 24 files to use shared HTTP client
- Replace .unwrap() with proper error handling
- Eliminate code duplication in HTTP requests

## Project Restructuring
- Rename package: ai → aibot
- Add dual binary support: aibot (main) + ai (compatibility alias)
- Migrate config directory: ~/.config/ai/ → ~/.config/syui/ai/bot/
- Implement backward compatibility with automatic migration

## Testing Infrastructure
- Add unit tests for HttpClient
- Create test infrastructure with cargo-make
- Add test commands: test, test-quick, test-verbose

## Documentation
- Complete migration guide with step-by-step instructions
- Updated development guide with new structure
- HTTP client API reference documentation
- Comprehensive refactoring summary

## Files Changed
- Modified: 24 source files (HTTP client integration)
- Added: src/http_client.rs, src/alias.rs, src/tests/
- Added: 5 documentation files in docs/
- Added: migration setup script

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-06 23:47:12 +09:00
998777d46a test game
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 29m13s
2025-01-22 17:54:42 +09:00
61d7df6922 fix login
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s
2025-01-20 17:31:45 +09:00
eb8f1b17c8 fix bc
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 18s
2025-01-20 17:30:34 +09:00
fc5e942f0c add card
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s
2024-12-27 16:40:11 +09:00
d2a394cec2 fix help
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2024-11-08 08:14:41 +09:00
27d5dac208 update gpt-4o-mini 2024-11-08 08:14:40 +09:00
ddd6f37118 fix check cid 2024-11-08 08:14:40 +09:00
14ca1bcdee check feed watch cid
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2024-08-18 02:17:58 +09:00
84efc31248 add feed watch
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2024-08-18 01:30:48 +09:00
3904c576f0 add cargo toml
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2024-08-04 08:10:53 +09:00
08436c0a56 fix docker
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s
2024-08-04 08:02:53 +09:00
d5603fda52 test planet 2024-08-04 08:02:53 +09:00
4633901ca0 test planet 2024-08-04 08:02:53 +09:00
a8fd189a63 fix bot len return 2024-08-04 08:02:53 +09:00
b540d0c007 fix down feed-generator
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2024-05-23 02:17:31 +09:00
bf31cf2a8f add bot comment system
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
2024-05-14 03:38:59 +09:00
840320d0d2 add bot custom feed
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s
2024-04-22 22:30:43 +09:00
5d60645c0f fix admin
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
2024-04-12 08:22:54 +09:00
e7c06cf9d1 test manga
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2024-04-04 13:53:39 +09:00
025b24b8f0 fix ten post
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2024-03-31 01:34:23 +09:00
7bbc3370d7 fix fortune
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2024-03-30 02:57:38 +09:00
d6777a0c6a fix sp 2024-03-30 02:57:38 +09:00
87a333d744 fix card handle 2024-03-30 02:57:38 +09:00
2f0bfe08b0 fix print 2024-03-30 02:57:37 +09:00
97856f3765 add host 2024-03-30 02:57:37 +09:00
7b03adda1f v0.1
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 9s
2024-03-05 04:09:43 +09:00
65 changed files with 3718 additions and 547 deletions

View File

@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(cargo check:*)",
"Bash(cargo test)",
"Bash(cargo test:*)",
"Bash(grep:*)",
"Bash(cargo install:*)",
"Bash(cargo make:*)",
"Bash(cargo:*)",
"Bash(ls:*)",
"Bash(./target/debug/aibot --help)",
"Bash(./target/debug/ai --help)",
"Bash(mkdir:*)",
"Bash(chmod:*)",
"Bash(git checkout:*)",
"Bash(git add:*)"
],
"deny": []
}
}

View File

@ -0,0 +1,18 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ gitea.status }}."

7
.gitignore vendored
View File

@ -1,6 +1,6 @@
Cargo.lock
target
*.json
#*.json
*.DS_Store
**.DS_Store
scpt/json/
@ -12,3 +12,8 @@ scpt/json/
.ssh/*.pub
.ssh/*config
.env
pnpm-lock.yaml
**Cargo.lock
*/target/
*/**/*.rs.bk

View File

@ -1,7 +1,17 @@
[package]
name = "ai"
version = "0.0.1"
name = "aibot"
authors = ["syui"]
version = "0.1.0"
edition = "2021"
description = "ai.bot - Bluesky AT Protocol Bot"
[[bin]]
name = "aibot"
path = "src/main.rs"
[[bin]]
name = "ai"
path = "src/alias.rs"
[dependencies]
seahorse = "*"
@ -17,3 +27,7 @@ rustc-serialize = "*"
toml = "*"
iso8601-timestamp = "*"
sysinfo = "*"
[dev-dependencies]
mockito = "1.2"
tokio-test = "0.4"

View File

@ -1,5 +1,4 @@
FROM syui/aios
ADD .ssh /root/.ssh
WORKDIR /root
ADD ./test/entrypoint.sh .

View File

@ -17,6 +17,14 @@ command = "cargo"
args = ["test"]
dependencies = ["clean"]
[tasks.test-quick]
command = "cargo"
args = ["test"]
[tasks.test-verbose]
command = "cargo"
args = ["test", "--", "--nocapture"]
[tasks.my-flow]
dependencies = [
"format",

View File

@ -10,7 +10,11 @@
$ ai
```
### logo
```sh
$ docker run -it syui/aios ai
```
### build
```sh
$ cargo build
@ -59,15 +63,19 @@ $ ai bot
|command|sub|type|link|auth|
|---|---|---|---|---|
|@yui.syui.ai did||mention, reply| [plc.directory](https://plc.directory)/$did/log |user|
|@yui.syui.ai card|r, s, b|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|@yui.syui.ai ten|start, d, p, coin|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|@yui.syui.ai fav|{cid}|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|@yui.syui.ai egg|{password}|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|@yui.syui.ai 占い||mention, reply| [yui.syui.ai](https://yui.syui.ai) |user|
|@yui.syui.ai nyan|🍰|mention, reply| [yui.syui.ai](https://yui.syui.ai) |user|
|@yui.syui.ai diffusers|{keyword}|mention, reply| [huggingface.co/diffusers](https://huggingface.co/docs/diffusers/index) |user|
|@yui.syui.ai sh|{command}|mention, reply| [archlinux.org](https://wiki.archlinux.org/title/Systemd-nspawn) |admin|
|/did||mention, reply| [plc.directory](https://plc.directory)/$did/log |user|
|/card|r, s, b|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|/ten|start, close, d, p|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|/fav|{cid}|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|/egg|{password}|mention, reply| [card.syui.ai](https://card.syui.ai) |user|
|/nyan|🍬|mention, reply| [yui.syui.ai](https://yui.syui.ai) |user|
|/diffusers|{keyword}|mention, reply| [huggingface.co/diffusers](https://huggingface.co/docs/diffusers/index) |user|
|/sh|{command}|mention, reply| [archlinux.org](https://wiki.archlinux.org/title/Systemd-nspawn) |admin|
|/占い||mention, reply| [yui.syui.ai](https://yui.syui.ai) |user|
```sh
@yui.syui.ai /did
```
### test
@ -84,7 +92,6 @@ $ cargo install --force cargo-make
$ cargo make build
```
### docker
> .env
@ -100,3 +107,38 @@ ADMIN=syui.syu.is
$ docker compose build
$ docker compose up -d
```
## pds:card
- https://atproto.com/ja/guides/lexicon
- https://at.syu.is/at/did:plc:uqzpqmrjnptsxezjx4xuh2mn/ai.syui.card/3lagpwihqxi2v
```sh
# oauth(button)
[yui]ai.syui.card.verify -> [user]ai.syui.card
[yui]
$ ./target/debug/ai card-verify -i 0 -p 0 -r 0 -h syui.ai -d did:plc:uqzpqmrjnptsxezjx4xuh2mn
{"uri":"at://did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.card.verify/3lagpvhppmd2q"}
[user]
$ ./target/debug/ai card -i 0 -p 0 -r 0 -v at://did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.card.verify/3lagpvhppmd2q
```
## pds:game
- https://atproto.com/ja/specs/record-key
- https://at.syu.is/at/did:plc:uqzpqmrjnptsxezjx4xuh2mn/ai.syui.game/self
```sh
# oauth(play)
[yui]ai.syui.game.user -> [user]ai.syui.game
[account]
# https://at.syu.is/at/did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.game.user/syui
## [rkey]
1. echo $handle|cut -d . -f 1
2. $handle
3. tid
```

10
at/feed-generator/env Normal file
View File

@ -0,0 +1,10 @@
FEEDGEN_PORT=3000
FEEDGEN_LISTENHOST="0.0.0.0"
FEEDGEN_SQLITE_LOCATION="/data/db.sqlite"
FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bgs.syu.is"
FEEDGEN_PUBLISHER_DID="did:web:feed.syu.is"
FEEDGEN_HOSTNAME="feed.syu.is"
FEEDGEN_SUBSCRIPTION_RECONNECT_DELAY=3000
FEEDGEN_PUBLISHER_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bsky.network"

View File

@ -0,0 +1,26 @@
# custom feed
- at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd
- [bsky.app](https://bsky.app/profile/did:plc:4hqjfn7m6n5hno3doamuhgef/feed/cmd)
- [app.bsky.feed.getFeedSkeleton](https://feed.syu.is/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd)
```sh
did=did:plc:4hqjfn7m6n5hno3doamuhgef
col=app.bsky.feed.generator
cid=cmd
uri=at://$did/$col/$cid
echo $uri
```
## bsky-feed
```sh
$ git clone https://github.com/bluesky-social/feed-generator
```
```sh
docker compose build feed-generator
docker build -t publish_feed -f Dockerfile.feed .
docker run publish_feed
```

View File

@ -0,0 +1,43 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
import { AppContext } from '../config'
// max 15 chars
export const shortname = 'cmd'
export const handler = async (ctx: AppContext, params: QueryParams) => {
let builder = ctx.db
.selectFrom('post')
.selectAll()
.orderBy('indexedAt', 'desc')
.orderBy('cid', 'desc')
.limit(params.limit)
if (params.cursor) {
const [indexedAt, cid] = params.cursor.split('::')
if (!indexedAt || !cid) {
throw new InvalidRequestError('malformed cursor')
}
const timeStr = new Date(parseInt(indexedAt, 10)).toISOString()
builder = builder
.where('post.indexedAt', '<', timeStr)
.orWhere((qb) => qb.where('post.indexedAt', '=', timeStr))
.where('post.cid', '<', cid)
}
const res = await builder.execute()
const feed = res.map((row) => ({
post: row.uri,
}))
let cursor: string | undefined
const last = res.at(-1)
if (last) {
cursor = `${new Date(last.indexedAt).getTime()}::${last.cid}`
}
return {
cursor,
feed,
}
}

View File

@ -0,0 +1,14 @@
import { AppContext } from '../config'
import {
QueryParams,
OutputSchema as AlgoOutput,
} from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
import * as cmd from './cmd'
type AlgoHandler = (ctx: AppContext, params: QueryParams) => Promise<AlgoOutput>
const algos: Record<string, AlgoHandler> = {
[cmd.shortname]: cmd.handler,
}
export default algos

View File

@ -0,0 +1,50 @@
import {
OutputSchema as RepoEvent,
isCommit,
} from './lexicon/types/com/atproto/sync/subscribeRepos'
import { FirehoseSubscriptionBase, getOpsByType } from './util/subscription'
export class FirehoseSubscription extends FirehoseSubscriptionBase {
async handleEvent(evt: RepoEvent) {
if (!isCommit(evt)) return
const ops = await getOpsByType(evt)
// This logs the text of every post off the firehose.
// Just for fun :)
// Delete before actually using
for (const post of ops.posts.creates) {
console.log(post.record.text)
}
const postsToDelete = ops.posts.deletes.map((del) => del.uri)
const postsToCreate = ops.posts.creates
.filter((create) => {
return create.record.text.match('^/[a-z]') || create.record.text.match('^@ai ') || create.record.text.match('/ai ');
//return create.record.text.toLowerCase().includes('alf')
})
.map((create) => {
// map alf-related posts to a db row
return {
uri: create.uri,
cid: create.cid,
replyParent: create.record?.reply?.parent.uri ?? null,
replyRoot: create.record?.reply?.root.uri ?? null,
indexedAt: new Date().toISOString(),
}
})
if (postsToDelete.length > 0) {
await this.db
.deleteFrom('post')
.where('uri', 'in', postsToDelete)
.execute()
}
if (postsToCreate.length > 0) {
await this.db
.insertInto('post')
.values(postsToCreate)
.onConflict((oc) => oc.doNothing())
.execute()
}
}
}

117
docs/README.md Normal file
View File

@ -0,0 +1,117 @@
# ai.bot ドキュメント
ai.botプロジェクトの包括的なドキュメント集です。
## ドキュメント一覧
### 開発者向け
1. **[開発ガイド](./development-guide.md)**
- プロジェクト概要とアーキテクチャ
- 開発環境のセットアップ
- 開発ワークフローとベストプラクティス
- 新機能追加の手順
2. **[HTTPクライアントAPI](./http-client-api.md)**
- HttpClientモジュールの完全なAPIリファレンス
- 使用例とサンプルコード
- エラーハンドリングのベストプラクティス
### 保守・運用向け
3. **[移行ガイド](./migration-guide.md)**
- パッケージ名・CLI名の変更詳細
- 段階的移行手順
- 後方互換性の説明
- トラブルシューティング
4. **[リファクタリングサマリー](./refactoring-summary.md)**
- HTTPクライアント共通化の詳細
- エラーハンドリング改善の記録
- 対象ファイル一覧とBefore/After
## クイックスタート
### 1. 基本セットアップ
```bash
# 依存関係インストール
cargo install cargo-make
# プロジェクトビルド
cargo build
# テスト実行
cargo test
```
### 2. CLI使用方法
```bash
# 新しいコマンド(推奨)
./target/debug/aibot --help
# 旧コマンド(互換性)
./target/debug/ai --help
```
### 3. 設定ディレクトリ
- **新**: `~/.config/syui/ai/bot/`
- **旧**: `~/.config/ai/` (自動移行対応)
## 主要な変更履歴
### 2025年6月6日 - 大規模リファクタリング
#### HTTPクライアント共通化
- 24個のファイルをHttpClientモジュールで統合
- コード重複を大幅削減
- エラーハンドリングを改善(`.unwrap()``match`
#### 命名規則統一
- パッケージ名: `ai``aibot`
- CLI名: `ai``aibot``ai`は互換性維持)
- 設定ディレクトリ: `~/.config/ai/``~/.config/syui/ai/bot/`
#### テストインフラ構築
- ユニットテストの追加
- cargo-makeによるタスク管理
- CI/CD対応の準備
## アーキテクチャ概要
```
ai.bot/
├── src/
│ ├── main.rs # メインCLI (aibot)
│ ├── alias.rs # 互換性CLI (ai)
│ ├── http_client.rs # 統合HTTPクライアント
│ ├── data.rs # 設定・データ管理
│ ├── game/ # ゲーム機能
│ └── tests/ # テストスイート
├── docs/ # ドキュメント
├── scripts/ # セットアップスクリプト
└── ~/.config/syui/ai/bot/ # 設定ディレクトリ
├── scpt/ # コマンドスクリプト
└── txt/ # ログファイル
```
## サポート・問い合わせ
### 開発関連
- 新機能追加: [開発ガイド](./development-guide.md)を参照
- API使用方法: [HTTPクライアントAPI](./http-client-api.md)を参照
### 移行・運用関連
- 移行作業: [移行ガイド](./migration-guide.md)を参照
- トラブル: 各ドキュメントのトラブルシューティング章を参照
### 履歴・詳細
- 実装詳細: [リファクタリングサマリー](./refactoring-summary.md)を参照
## 貢献ガイドライン
1. **コードスタイル**: `cargo fmt`でフォーマット必須
2. **テスト**: 新機能には対応するテストを追加
3. **エラーハンドリング**: `.unwrap()`の使用禁止
4. **ドキュメント**: 重要な変更は対応ドキュメントも更新
詳細は[開発ガイド](./development-guide.md)の「コントリビューション」章を参照してください。

38
docs/atproto.md Normal file
View File

@ -0,0 +1,38 @@
## curl
no-authorization
https://docs.bsky.app/docs/api/com-atproto-repo-describe-repo
```sh
handle=yui.syui.ai
host=bsky.social
api=$host/xrpc
plc=plc.directory
url=$api/com.atproto.repo.describeRepo
curl -sL ${host}/xrpc/_health
d=`curl -sL "${url}?repo=$handle"`
echo $d
did=`echo $d|jq -r .did`
echo $did
collection=app.bsky.feed.post
url=$api/com.atproto.repo.listRecords
timed=`curl -sL "${url}?repo=$handle&collection=$collection&reverse=true&limit=1"|jq -r ".[]|.[0]?|.value.createdAt"`
cid=`curl -sL "${url}?repo=$handle&collection=$collection&reverse=true&limit=1"|jq -r ".[]|.[0]?|.cid"`
uri=`curl -sL "${url}?repo=$handle&collection=$collection&reverse=true&limit=1"|jq -r ".[]|.[0]?|.uri"`
rkey=`echo $uri|cut -d / -f 5`
url=$api/com.atproto.repo.getRecord
curl -sL "$url?repo=$did&collection=$collection&rkey=$rkey"|jq .
uri=at://did:plc:vjug55kidv6sye7ykr5faxxn/app.bsky.feed.post/3jzn6g7ixgq2y
cid=bafyreiey2tt4dhvuvr7tofatdverqrxmscnnus2uyfcmkacn2fov3vb4wa
did=did:plc:vjug55kidv6sye7ykr5faxxn
rkey=3jzn6g7ixgq2y
url=$api/com.atproto.repo.getRecord
curl -sL "$url?repo=$did&collection=$collection&rkey=$rkey&cid="|jq .
```

332
docs/development-guide.md Normal file
View File

@ -0,0 +1,332 @@
# ai.bot 開発ガイド
## プロジェクト概要
ai.botは、Rust製のBlueskyAT Protocolボットです。メンション応答、コマンド実行、OpenAI統合などの機能を提供します。
**重要**: 2025年6月6日より、パッケージ名とCLI名が統一されました
- **新CLI名**: `aibot` (推奨)
- **旧CLI名**: `ai` (互換性維持)
- **設定ディレクトリ**: `~/.config/syui/ai/bot/` (旧: `~/.config/ai/`)
詳細は[移行ガイド](./migration-guide.md)を参照してください。
## アーキテクチャ
### 主要コンポーネント
1. **HTTPクライアント** (`src/http_client.rs`)
- AT Protocol API呼び出しの統一インターフェース
- 認証処理の自動化
- エラーハンドリングの標準化
2. **コマンドシステム** (`src/main.rs`)
- Seahorseを使用したCLIインターフェース
- 各機能への振り分け
3. **AT Protocolモジュール**
- 投稿、フォロー、いいね等の基本機能
- 認証・セッション管理
- フィード・通知処理
4. **ゲームシステム** (`src/game/`)
- カードゲーム機能
- ユーザー管理
- ゲームデータ処理
5. **外部連携**
- OpenAI API統合 (`src/openai.rs`)
- 画像処理機能
## 開発環境セットアップ
### 必要なツール
```bash
# Rust最新安定版
rustup update stable
# Cargo makeタスクランナー
cargo install cargo-make
# 開発用依存関係は自動インストール
```
**注意**: 初回は`cargo install cargo-make`でcargo-makeのインストールが必要です。
### 設定ファイル
```
~/.config/syui/ai/bot/config.toml # 基本設定
~/.config/syui/ai/bot/refresh # リフレッシュトークン
~/.config/syui/ai/bot/access # アクセストークン
```
**注意**: 旧設定ディレクトリ(`~/.config/ai/`)も自動的に参照・移行されます。
## 開発ワークフロー
### 1. コード変更
```bash
# フォーマット
cargo fmt
# コンパイル確認
cargo check
# テスト実行
cargo test
```
### 2. 統合ワークフロー
```bash
# 全体フロー(フォーマット→ビルド→テスト)
cargo make my-flow
```
### 3. 個別テスト
```bash
cargo make test-quick # 素早いテスト
cargo make test-verbose # 詳細出力テスト
```
## API呼び出しパターン
### HttpClientの使用方法
#### 基本的なGETリクエスト
```rust
use crate::http_client::HttpClient;
pub async fn get_user_profile(handle: String) -> String {
let client = HttpClient::new();
let url = format!("https://bsky.social/xrpc/app.bsky.actor.getProfile?actor={}", handle);
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error getting profile: {}", e);
"err".to_string()
}
}
}
```
#### JSONを送信するPOSTリクエスト
```rust
use crate::http_client::HttpClient;
use serde_json::json;
pub async fn create_post(text: String) -> String {
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.repo.createRecord";
let payload = json!({
"repo": "user.handle",
"collection": "app.bsky.feed.post",
"record": {
"text": text,
"createdAt": chrono::Utc::now().to_rfc3339()
}
});
match client.post_json_with_auth(&url, &payload).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error creating post: {}", e);
"err".to_string()
}
}
}
```
## テストの書き方
### ユニットテスト
```rust
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_http_client_creation() {
let client = HttpClient::new();
// テストロジック
}
#[test]
fn test_data_parsing() {
// 同期テスト
}
}
```
### 統合テスト
```rust
#[tokio::test]
#[ignore] // 通常は無視、環境変数設定時のみ実行
async fn test_real_api() {
if std::env::var("RUN_INTEGRATION_TESTS").is_ok() {
// 実際のAPI呼び出しテスト
}
}
```
## エラーハンドリングガイドライン
### 推奨パターン
```rust
// Good: 適切なエラーハンドリング
match api_call().await {
Ok(response) => response,
Err(e) => {
eprintln!("Error in operation: {}", e);
"err".to_string()
}
}
// Bad: unwrapの使用
api_call().await.unwrap()
```
### エラーレスポンス
- API呼び出し失敗時は`"err"`文字列を返す
- ログにエラー詳細を出力
- 上位レイヤーでエラー処理を継続
## 新機能追加の手順
### 1. AT Protocol関連機能
```bash
# 1. モジュールファイルを作成
touch src/new_feature.rs
# 2. main.rsに追加
# pub mod new_feature;
# 3. HttpClientを使用して実装
# 4. テストを追加
# 5. main.rsのコマンドに追加
```
### 2. 基本的なテンプレート
```rust
use crate::http_client::HttpClient;
use crate::{data_toml, url};
use serde_json::json;
pub async fn new_feature_request(param: String) -> String {
let client = HttpClient::new();
let endpoint_url = url(&"endpoint_name");
let payload = json!({
"param": param
});
match client.post_json_with_auth(&endpoint_url, &payload).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error in new_feature: {}", e);
"err".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_new_feature() {
// テスト実装
}
}
```
## デバッグ方法
### 1. ログ出力
```rust
eprintln!("Debug: {}", variable);
println!("Info: {}", message);
```
### 2. テスト実行
```bash
# 特定のテストのみ
cargo test test_name
# 詳細出力
cargo test -- --nocapture
# 特定モジュール
cargo test module_name
```
### 3. HTTPリクエストのデバッグ
HttpClientモジュール内でリクエスト/レスポンスをログ出力することで、API呼び出しの詳細を確認できます。
## パフォーマンス考慮事項
### HttpClientの再利用
```rust
// Good: 一度作成して再利用
let client = HttpClient::new();
for item in items {
client.get_with_auth(&url).await;
}
// Bad: 毎回新規作成
for item in items {
let client = HttpClient::new();
client.get_with_auth(&url).await;
}
```
### 非同期処理
- 可能な限り並列処理を活用
- await呼び出しを最小限に
- tokio::joinやfutures::join_allの活用
## トラブルシューティング
### よくある問題
1. **認証エラー**
- トークンの有効期限切れ
- 設定ファイルの不備
2. **コンパイルエラー**
- 型不整合(特にライフタイム)
- 未使用のimport
3. **実行時エラー**
- ネットワーク接続問題
- API仕様変更
### 解決方法
```bash
# 設定確認(新ディレクトリ)
ls -la ~/.config/syui/ai/bot/
# 旧ディレクトリも確認
ls -la ~/.config/ai/
# トークンリフレッシュ
./target/debug/aibot refresh
# または互換性コマンド
./target/debug/ai refresh
# 詳細ログ
RUST_LOG=debug ./target/debug/aibot [command]
```
## コントリビューション
1. コードフォーマット必須: `cargo fmt`
2. テスト追加必須: 新機能には対応テスト
3. エラーハンドリング必須: `.unwrap()`の使用禁止
4. ドキュメント更新: 重要な変更は本ドキュメントも更新
## 関連ドキュメント
- [リファクタリングサマリー](./refactoring-summary.md)
- [AT Protocol仕様](https://atproto.com/)
- [Rust公式ドキュメント](https://doc.rust-lang.org/)

334
docs/http-client-api.md Normal file
View File

@ -0,0 +1,334 @@
# HttpClient API リファレンス
## 概要
`HttpClient`は、AT Protocol APIへの統一されたHTTPクライアントインターフェースです。認証、エラーハンドリング、リクエスト管理を自動化します。
## 基本的な使用方法
```rust
use crate::http_client::HttpClient;
let client = HttpClient::new();
// または
let client = HttpClient::default();
```
## APIメソッド
### 認証付きリクエスト
#### get_with_auth
AT Protocolの認証が必要なGETリクエストを実行します。
```rust
pub async fn get_with_auth(&self, url: &str) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
**戻り値:**
- `Ok(String)`: レスポンスボディ(文字列)
- `Err(Error)`: リクエストエラー
**使用例:**
```rust
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/app.bsky.actor.getProfile?actor=user.bsky.social";
match client.get_with_auth(&url).await {
Ok(response) => println!("Response: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
#### post_json_with_auth
AT Protocolの認証が必要なPOSTリクエストJSONを実行します。
```rust
pub async fn post_json_with_auth<T: Serialize>(&self, url: &str, json: &T) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
- `json`: シリアライズ可能なJSONデータ
**戻り値:**
- `Ok(String)`: レスポンスボディ(文字列)
- `Err(Error)`: リクエストエラー
**使用例:**
```rust
use serde_json::json;
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.repo.createRecord";
let payload = json!({
"repo": "user.bsky.social",
"collection": "app.bsky.feed.post",
"record": {
"text": "Hello, World!",
"createdAt": "2025-01-01T00:00:00Z"
}
});
match client.post_json_with_auth(&url, &payload).await {
Ok(response) => println!("Post created: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
#### delete_with_auth
AT Protocolの認証が必要なDELETEリクエストを実行します。
```rust
pub async fn delete_with_auth(&self, url: &str) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
**戻り値:**
- `Ok(String)`: レスポンスボディ(文字列)
- `Err(Error)`: リクエストエラー
**使用例:**
```rust
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.repo.deleteRecord";
match client.delete_with_auth(&url).await {
Ok(response) => println!("Deleted: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
### 認証なしリクエスト
#### get
認証なしのGETリクエストを実行します。
```rust
pub async fn get(&self, url: &str) -> Result<String, Error>
```
**使用例:**
```rust
let client = HttpClient::new();
let url = "https://public-api.example.com/data";
match client.get(&url).await {
Ok(response) => println!("Response: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
#### post_json
認証なしのPOSTリクエストJSONを実行します。ログイン処理などで使用。
```rust
pub async fn post_json<T: Serialize>(&self, url: &str, json: &T) -> Result<String, Error>
```
**使用例:**
```rust
use serde_json::json;
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.session.create";
let credentials = json!({
"identifier": "user.bsky.social",
"password": "password"
});
match client.post_json(&url, &credentials).await {
Ok(response) => println!("Login successful: {}", response),
Err(e) => eprintln!("Login failed: {}", e),
}
```
### カスタムヘッダー付きリクエスト
#### post_with_headers
カスタムヘッダーを指定したPOSTリクエストを実行します。
```rust
pub async fn post_with_headers<T: Serialize>(
&self,
url: &str,
json: &T,
headers: Vec<(&str, &str)>
) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
- `json`: シリアライズ可能なJSONデータ
- `headers`: ヘッダーのキーと値のペアのベクター
**使用例:**
```rust
use serde_json::json;
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.session.refresh";
let refresh_token = "refresh_token_value";
let auth_header = format!("Bearer {}", refresh_token);
let headers = vec![("Authorization", auth_header.as_str())];
let empty_json = json!({});
match client.post_with_headers(&url, &empty_json, headers).await {
Ok(response) => println!("Token refreshed: {}", response),
Err(e) => eprintln!("Refresh failed: {}", e),
}
```
## 内部実装詳細
### 認証処理
認証付きメソッドは自動的に以下を実行します:
1. `data_refresh(&"access")`でアクセストークンを取得
2. `Authorization: Bearer {token}`ヘッダーを自動追加
3. リクエストを実行
### エラーハンドリング
- ネットワークエラー
- HTTPステータスエラー
- JSON解析エラー
- タイムアウトエラー
すべて`reqwest::Error`として統一されて返されます。
## 使用上の注意
### 1. トークン管理
- アクセストークンは自動的に取得されます
- トークンの有効期限切れは呼び出し元で処理する必要があります
- リフレッシュトークンは`post_with_headers`で手動設定
### 2. エラー処理パターン
```rust
// 推奨パターン
match client.get_with_auth(&url).await {
Ok(response) => {
// 成功処理
response
},
Err(e) => {
eprintln!("API call failed: {}", e);
"err".to_string() // 既存コードとの互換性
}
}
```
### 3. パフォーマンス
- `HttpClient`の作成は軽量な操作です
- 内部でrequwestクライアントを再利用しています
- 複数のリクエストで同じインスタンスを使い回すことも可能
### 4. デバッグ
リクエスト/レスポンスの詳細をログ出力したい場合:
```rust
// HttpClientモジュール内で適宜printlnを追加
println!("Request URL: {}", url);
println!("Response: {}", response);
```
## 実装例新しいAPI呼び出しモジュール
```rust
use crate::http_client::HttpClient;
use crate::{data_toml, url};
use serde_json::json;
use iso8601_timestamp::Timestamp;
pub async fn create_custom_record(data: String) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let timestamp = Timestamp::now_utc().to_string();
let payload = json!({
"repo": handle,
"did": did,
"collection": "app.bsky.custom.record",
"record": {
"data": data,
"createdAt": timestamp
}
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &payload).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error creating custom record: {}", e);
"err".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_custom_record() {
let result = create_custom_record("test data".to_string()).await;
assert_ne!(result, "err");
}
}
```
## 移行ガイド
### 既存コードからの移行
#### Before (旧実装)
```rust
extern crate reqwest;
use crate::data_refresh;
pub async fn old_request() -> String {
let token = data_refresh(&"access");
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&data)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
res
}
```
#### After (新実装)
```rust
use crate::http_client::HttpClient;
pub async fn new_request() -> String {
let client = HttpClient::new();
match client.post_json_with_auth(&url, &data).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error in request: {}", e);
"err".to_string()
}
}
}
```
### チェックリスト
- [ ] `extern crate reqwest;`を削除
- [ ] `use crate::http_client::HttpClient;`を追加
- [ ] `data_refresh(&"access")`の手動呼び出しを削除
- [ ] `reqwest::Client::new()``HttpClient::new()`に置換
- [ ] `.unwrap()`を適切な`match`文に置換
- [ ] エラーメッセージの追加

271
docs/migration-guide.md Normal file
View File

@ -0,0 +1,271 @@
# ai.bot 段階的移行ガイド
## 概要
ai.botプロジェクトの命名規則とディレクトリ構造を統一するための段階的移行作業の記録です。
## 移行内容
### 1. パッケージ・CLI名の変更
| 項目 | 変更前 | 変更後 |
|------|--------|--------|
| パッケージ名 | `ai` | `aibot` |
| メインCLI | `ai` | `aibot` |
| 互換性CLI | - | `ai` (aibotへのエイリアス) |
### 2. ディレクトリ構造の変更
| 用途 | 変更前 | 変更後 |
|------|--------|--------|
| 設定ディレクトリ | `~/.config/ai/` | `~/.config/syui/ai/bot/` |
| ログディレクトリ | `~/.config/ai/txt/` | `~/.config/syui/ai/bot/txt/` |
| スクリプトディレクトリ | `~/.config/ai/scpt/` | `~/.config/syui/ai/bot/scpt/` |
## 実装した機能
### 1. デュアルバイナリシステム
#### Cargo.toml設定
```toml
[package]
name = "aibot"
description = "ai.bot - Bluesky AT Protocol Bot"
[[bin]]
name = "aibot"
path = "src/main.rs"
[[bin]]
name = "ai"
path = "src/alias.rs"
```
#### エイリアスバイナリ (src/alias.rs)
- `ai`コマンドが`aibot`を自動的に呼び出し
- 同一ディレクトリ内のaibotバイナリを優先検索
- PATH内のaibotもフォールバック対応
### 2. 設定ディレクトリの自動移行
#### data_file関数の改良
```rust
pub fn data_file(s: &str) -> String {
// 新しい設定ディレクトリ(優先)
let new_config_dir = "/.config/syui/ai/bot/";
// 旧設定ディレクトリ(互換性のため)
let old_config_dir = "/.config/ai/";
// 自動移行ロジック
// 1. 新しいパスにファイルが存在 → 新しいパスを使用
// 2. 旧パスのみに存在 → 新しいパスにコピーして使用
// 3. どちらにも存在しない → 新しいパスを使用
}
```
#### log_file関数も同様の移行対応
### 3. 移行セットアップスクリプト
#### scripts/setup-migration.sh
```bash
#!/bin/bash
# 新しい設定ディレクトリの作成
mkdir -p ~/.config/syui/ai/bot/
# スクリプトディレクトリのコピー
cp -r ~/.config/ai/scpt ~/.config/syui/ai/bot/
# エイリアス設定の提案
echo "alias ai='aibot'"
```
## 後方互換性
### 1. 設定ファイル
- **自動移行**: 旧パスから新パスへ自動コピー
- **継続読み込み**: 移行後も旧パスは参照可能
- **透過的**: ユーザーは変更を意識する必要なし
### 2. コマンドライン
- **aiコマンド**: 既存スクリプトで引き続き使用可能
- **aibotコマンド**: 新しい正式名称
- **完全互換**: 引数、オプション、出力すべて同一
### 3. スクリプト
- **shellscript**: `ai`コマンドをそのまま使用可能
- **エイリアス推奨**: `alias ai='aibot'`で統一
## 移行手順
### 1. 自動移行(推奨)
```bash
# プロジェクトをビルド
cargo build
# 移行スクリプトを実行
./scripts/setup-migration.sh
# 新しいバイナリを使用
./target/debug/aibot --help
./target/debug/ai --help # 互換性確認
```
### 2. 手動移行
```bash
# 1. 新しい設定ディレクトリ作成
mkdir -p ~/.config/syui/ai/bot/
# 2. スクリプトディレクトリのコピー
cp -r ~/.config/ai/scpt ~/.config/syui/ai/bot/
# 3. バイナリのインストール
cargo install --path .
# 4. エイリアス設定(オプション)
echo "alias ai='aibot'" >> ~/.zshrc
```
### 3. 段階的移行(企業環境等)
```bash
# Phase 1: 新しいバイナリの導入
cargo install --path . --bin aibot
# Phase 2: エイリアス設定
echo "alias ai='aibot'" >> ~/.profile
# Phase 3: スクリプトの段階的更新
# (既存スクリプトは変更不要)
# Phase 4: 旧設定の完全移行(任意のタイミング)
rm -rf ~/.config/ai/ # 十分な検証後
```
## 影響範囲
### 1. 変更が必要な箇所
-**なし** (完全後方互換)
### 2. 変更が推奨される箇所
- 📝 shellrcでのエイリアス設定
- 📝 ドキュメント内のコマンド例
- 📝 CI/CDスクリプト新しい名前使用
### 3. 変更が不要な箇所
- ✅ 既存のshellscript
- ✅ 設定ファイル
- ✅ ログファイル
- ✅ git submodule
## トラブルシューティング
### 1. 設定ファイルが見つからない
```bash
# 現在の設定ディレクトリを確認
ls -la ~/.config/syui/ai/bot/
ls -la ~/.config/ai/ # 旧ディレクトリ
# 手動でコピー
cp ~/.config/ai/token.json ~/.config/syui/ai/bot/
```
### 2. aiコマンドが動作しない
```bash
# aibotバイナリの存在確認
which aibot
./target/debug/aibot --help
# エイリアスの設定
alias ai='aibot'
# または
export PATH="$(pwd)/target/debug:$PATH"
```
### 3. スクリプトディレクトリが見つからない
```bash
# 手動でコピー
cp -r ~/.config/ai/scpt ~/.config/syui/ai/bot/
# gitサブモジュールの場合
cd ~/.config/syui/ai/bot/scpt
git remote -v # リモートURLを確認
```
## 検証方法
### 1. 基本動作確認
```bash
# 新しいコマンド
aibot --help
aibot post "Test from aibot"
# 互換性確認
ai --help
ai post "Test from ai alias"
# 設定ディレクトリ確認
ls -la ~/.config/syui/ai/bot/
```
### 2. 自動移行確認
```bash
# 旧設定で動作確認
touch ~/.config/ai/test_file
aibot some_command # 新ディレクトリにコピーされることを確認
ls -la ~/.config/syui/ai/bot/test_file
```
### 3. スクリプト互換性確認
```bash
# 既存スクリプトの動作確認
cd ~/.config/syui/ai/bot/scpt
./ai.zsh # aiコマンドが正常動作することを確認
```
## 今後の予定
### Phase 1 (完了)
- ✅ パッケージ名の変更
- ✅ デュアルバイナリシステム
- ✅ 設定ディレクトリの自動移行
- ✅ 後方互換性の確保
### Phase 2 (将来)
- 📅 ドキュメントの更新
- 📅 CI/CDの新コマンド対応
- 📅 パフォーマンス最適化
### Phase 3 (任意)
- 📅 旧設定ディレクトリの削除
- 📅 エイリアスバイナリの削除
- 📅 完全な新体系への移行
## 関連ファイル
- `Cargo.toml` - パッケージ設定
- `src/alias.rs` - エイリアスバイナリ
- `src/data.rs` - 設定ディレクトリ管理
- `scripts/setup-migration.sh` - 移行スクリプト
- `docs/migration-guide.md` - 本ドキュメント
## 注意事項
1. **gitサブモジュール**: scptディレクトリがgitサブモジュールの場合、リモートURLの確認が必要
2. **権限**: 設定ディレクトリの権限は適切に設定すること
3. **バックアップ**: 重要な設定は移行前にバックアップを推奨
4. **テスト**: 本番環境での使用前に十分なテストを実施
## 参考情報
- [開発ガイド](./development-guide.md)
- [リファクタリングサマリー](./refactoring-summary.md)
- [HTTPクライアントAPI](./http-client-api.md)

208
docs/refactoring-summary.md Normal file
View File

@ -0,0 +1,208 @@
# ai.bot リファクタリング作業サマリー
## 概要
2025年6月6日に実施されたai.botプロジェクトのリファクタリング作業の完全な記録です。
## 実施した作業
### 1. HTTPクライアントの共通化
- **作成したファイル**: `src/http_client.rs`
- **対象**: 24個のファイルを統合
- **削減**: 重複するHTTPリクエストコードを一箇所に集約
#### HttpClientモジュールの機能
```rust
// 認証付きリクエスト
client.get_with_auth(&url).await
client.post_json_with_auth(&url, &json_data).await
client.delete_with_auth(&url).await
// 認証なしリクエスト
client.get(&url).await
client.post_json(&url, &json_data).await
// カスタムヘッダー付きリクエスト
client.post_with_headers(&url, &json_data, headers).await
```
### 2. リファクタリング対象ファイル一覧
#### コアAPIファイル (8個)
- `src/post.rs` - 投稿作成
- `src/like.rs` - いいね機能
- `src/repost.rs` - リポスト機能
- `src/follow.rs` - フォロー/アンフォロー
- `src/reply.rs` - 返信機能
- `src/profile.rs` - プロフィール取得
- `src/delete_record.rs` - レコード削除
- `src/describe.rs` - ユーザー説明取得
#### 認証・セッション管理 (3個)
- `src/session.rs` - セッション管理
- `src/refresh.rs` - トークンリフレッシュ
- `src/token.rs` - ログイン認証
#### 通知・メンション (3個)
- `src/mention.rs` - メンション投稿
- `src/notify.rs` - 通知取得
- `src/notify_read.rs` - 通知既読
#### フィード・タイムライン (4個)
- `src/feed_get.rs` - フィード取得
- `src/timeline_author.rs` - 作者タイムライン
- `src/followers.rs` - フォロワー取得
- `src/follows.rs` - フォロー取得
#### 画像関連 (3個)
- `src/img.rs` - 画像投稿
- `src/img_reply.rs` - 画像付き返信
- `src/img_upload.rs` - 画像アップロード
#### リンク・リッチテキスト (3個)
- `src/post_link.rs` - リンク付き投稿
- `src/reply_link.rs` - リンク付き返信
- `src/reply_og.rs` - OGデータ付き返信
#### ゲームモジュール (5個)
- `src/game/post_card.rs` - ゲームカード投稿
- `src/game/post_card_verify.rs` - カード検証
- `src/game/post_game.rs` - ゲームデータ投稿
- `src/game/post_game_login.rs` - ゲームログイン
- `src/game/post_game_user.rs` - ゲームユーザーデータ
### 3. 除外したファイル
- `src/openai.rs` - OpenAI API用異なる認証方式のため
- `src/feed_watch.rs` - reqwest使用していないため
### 4. 共通の変更パターン
#### Before (変更前)
```rust
extern crate reqwest;
use crate::data_refresh;
pub async fn some_request() -> String {
let token = data_refresh(&"access");
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
res
}
```
#### After (変更後)
```rust
use crate::http_client::HttpClient;
pub async fn some_request() -> String {
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error: {}", e);
"err".to_string()
}
}
}
```
### 5. テストインフラの追加
#### 作成したテストファイル
- `src/tests/mod.rs` - テストモジュール宣言
- `src/tests/http_client_tests.rs` - HttpClientのテスト
#### テストコマンド
```bash
# 基本テスト
cargo test
# Makefileを使用したテスト
cargo make test # cleanしてからテスト
cargo make test-quick # 素早いテスト
cargo make test-verbose # 詳細出力
```
#### 追加した依存関係 (Cargo.toml)
```toml
[dev-dependencies]
mockito = "1.2"
tokio-test = "0.4"
```
## 改善された点
### コード品質
- **コード重複の削除**: 同じHTTPリクエストパターンの重複を排除
- **エラーハンドリング**: `.unwrap()`を適切な`match`文に置換
- **保守性**: HTTP関連のロジックが一箇所に集約
### 開発効率
- **変更容易性**: HTTPクライアントの変更が1ファイルで完結
- **テスト可能性**: ユニットテストの追加でバグ検出が容易
- **デバッグ性**: エラーメッセージの改善
### 安定性
- **パニック回避**: `.unwrap()`によるパニックを防止
- **エラー処理**: 適切なエラーレスポンスの返却
## 次のステップ(推奨)
### 1. bot.rsのリファクタリング (高優先度)
- 500行以上の巨大な関数を分割
- コマンド処理の構造化
- 深いif-else文の改善
### 2. 設定管理の統一 (中優先度)
- 複数の設定構造体の統合
- 設定ファイルパスの一元管理
### 3. main.rsの整理 (中優先度)
- コマンド定義の外部ファイル化
- モジュール構造の改善
## ファイル構造
```
src/
├── http_client.rs # 新規作成共通HTTPクライアント
├── tests/ # 新規作成:テストディレクトリ
│ ├── mod.rs
│ └── http_client_tests.rs
├── main.rs # 更新http_clientモジュール追加
├── Cargo.toml # 更新:テスト依存関係追加
├── Makefile.toml # 更新:テストコマンド追加
└── [24個のリファクタリング済みファイル]
```
## 注意事項
1. **OpenAI API**: `src/openai.rs`は意図的に除外(異なる認証方式)
2. **後方互換性**: 既存のAPI呼び出しは同じ形式を維持
3. **エラー処理**: "err"文字列を返すパターンは既存仕様に合わせて維持
## 検証方法
```bash
# コンパイル確認
cargo check
# テスト実行
cargo test
# フォーマット確認
cargo fmt --check
# 全体ビルド
cargo build
```
この作業により、ai.botプロジェクトのHTTP通信部分が大幅に改善され、今後の開発・保守が容易になりました。

View File

@ -1,4 +1,11 @@
### docker
## test-notify
```sh
./target/debug/ai n|jq -r ".notifications|.[].cid" >> ~/.config/ai/txt/notify_cid*
./target/debug/ai bot
```
## docker
```sh
$ docker run -it syui/aios ai
@ -11,7 +18,7 @@ $ cp -rf ~/.config/ai ./.config/
$ docker compose up
```
### cron
## cron
```sh
$ sudo pacman -S fcron
@ -19,7 +26,7 @@ $ fcrontab -e
* * * * * $HOME/bot/test/ai.zsh c
```
### ssh
## ssh
```sh
$ ssh-keygen -f /.ssh/diffusers.key -t ed25519
@ -50,3 +57,174 @@ services:
- ./.config:/root/.config
command: ai bot -a syui.syu.is
```
## openapi
```sh
# https://github.com/rdmurphy/atproto-openapi-types
$ curl -sLO https://raw.githubusercontent.com/rdmurphy/atproto-openapi-types/main/spec/api.json
```
## plc
```sh
# 何度か実行するとplcをlatestまでexportされる
$ .config/ai/scpt/test/pds.zsh e
```
## cmt
blogなどにblueskyアカウントのpostを表示します。
以下でbotがblogのコメントシステムを開きます。
```sh
@yui.syui.ai /comment https://syui.ai/blog/post/2024/04/25/bluesky/
```
開いたbotのpostに返信することで、特定のblog path上でpostを表示します。
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.post/3kqxbtmwlje2h" data-bluesky-cid="bafyreiasxp5g3nkkd6g7lxh55qaxcc7ylefaljmbcp627nu2geks62c57m"><p lang="">please reply with your comments here ↓
</p>&mdash; ai (<a href="https://bsky.app/profile/did:plc:4hqjfn7m6n5hno3doamuhgef?ref_src=embed">@yui.syui.ai</a>) <a href="https://bsky.app/profile/did:plc:4hqjfn7m6n5hno3doamuhgef/post/3kqxbtmwlje2h?ref_src=embed">Apr 25, 2024 at 20:18</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
```ts
<link href="https://syui.ai/js/comment/app.js" rel="preload" as="script">
<link href="https://syui.ai/js/comment/chunk-vendors.js" rel="preload" as="script">
<div id="comment"></div>
<script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
<script src="https://syui.ai/js/comment/chunk-vendors.js"></script>
<script src="https://syui.ai/js/comment/app.js"></script>
```
## example json
```json
[
{
"uri": "at://did:plc:wkzuqomvkxx5eiv5nl2lvm23/app.bsky.feed.post/3kp4ze5dcek2j",
"cid": "bafyreic4g7mthhw654zgv4skt5tqbs2xqg6n7bli4gayl2nquljngnotiy",
"author": {
"did": "did:plc:wkzuqomvkxx5eiv5nl2lvm23",
"handle": "syui.syu.is",
"displayName": "syui",
"avatar": "https://api.syu.is/img/avatar/plain/did:plc:wkzuqomvkxx5eiv5nl2lvm23/bafkreifvabvstfgawt6csagh44xdevb6c2uiwpgfho3xnpdrr6o7nbkxry@jpeg",
"indexedAt": "2024-01-14T10:20:13.367Z",
"viewer": {
"muted": false,
"blockedBy": false,
"following": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.graph.follow/3kiztjatnms25",
"followedBy": "at://did:plc:wkzuqomvkxx5eiv5nl2lvm23/app.bsky.graph.follow/3kirwsboeos26"
},
"labels": []
},
"reason": "reply",
"reasonSubject": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.feed.post/3kp4zdnlo5s2j",
"record": {
"text": "1",
"$type": "app.bsky.feed.post",
"langs": [
"ja"
],
"reply": {
"root": {
"cid": "bafyreiceckunxajycacn7dbuojrwb2wmurhfkleermvewwik44cn6vqo3a",
"uri": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.feed.post/3kp4zdnlo5s2j"
},
"parent": {
"cid": "bafyreiceckunxajycacn7dbuojrwb2wmurhfkleermvewwik44cn6vqo3a",
"uri": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.feed.post/3kp4zdnlo5s2j"
}
},
"createdAt": "2024-04-02T07:12:28.799Z"
},
"isRead": true,
"indexedAt": "2024-04-02T07:12:28.799Z",
"labels": []
},
{
"uri": "at://did:plc:wkzuqomvkxx5eiv5nl2lvm23/app.bsky.feed.post/3kp54af2zes2j",
"cid": "bafyreig4kvfpu557qehttt2y5eh7rcyodbxqwtnl73f3fhjsstiap3abzu",
"author": {
"did": "did:plc:wkzuqomvkxx5eiv5nl2lvm23",
"handle": "syui.syu.is",
"displayName": "syui",
"avatar": "https://api.syu.is/img/avatar/plain/did:plc:wkzuqomvkxx5eiv5nl2lvm23/bafkreifvabvstfgawt6csagh44xdevb6c2uiwpgfho3xnpdrr6o7nbkxry@jpeg",
"indexedAt": "2024-01-14T10:20:13.367Z",
"viewer": {
"muted": false,
"blockedBy": false,
"following": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.graph.follow/3kiztjatnms25",
"followedBy": "at://did:plc:wkzuqomvkxx5eiv5nl2lvm23/app.bsky.graph.follow/3kirwsboeos26"
},
"labels": []
},
"reason": "reply",
"reasonSubject": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.feed.post/3kp4zdnlo5s2j",
"record": {
"text": "2",
"$type": "app.bsky.feed.post",
"langs": [
"ja"
],
"reply": {
"root": {
"cid": "bafyreiceckunxajycacn7dbuojrwb2wmurhfkleermvewwik44cn6vqo3a",
"uri": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.feed.post/3kp4zdnlo5s2j"
},
"parent": {
"cid": "bafyreiceckunxajycacn7dbuojrwb2wmurhfkleermvewwik44cn6vqo3a",
"uri": "at://did:plc:dconvttcori3mrh2wrmehvwt/app.bsky.feed.post/3kp4zdnlo5s2j"
}
},
"createdAt": "2024-04-02T08:04:03.938Z"
},
"isRead": true,
"indexedAt": "2024-04-02T08:04:03.938Z",
"labels": []
}
]
```
```json
{
"uri": "at://did:plc:uqzpqmrjnptsxezjx4xuh2mn/app.bsky.feed.post/3kp5qniyzm42h",
"cid": "bafyreihmutmtf2clpgmx5l3qpu6xea6z25xrop74mltsycs5lfacm27u6e",
"author": {
"did": "did:plc:uqzpqmrjnptsxezjx4xuh2mn",
"handle": "syui.ai",
"displayName": "syui",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:uqzpqmrjnptsxezjx4xuh2mn/bafkreid6kcc5pnn4b3ar7mj6vi3eiawhxgkcrw3edgbqeacyrlnlcoetea@jpeg",
"viewer": {
"muted": false,
"blockedBy": false,
"followedBy": "at://did:plc:uqzpqmrjnptsxezjx4xuh2mn/app.bsky.graph.follow/3kkvst5iq6r2a"
},
"labels": [],
"description": "https://syui.ai",
"indexedAt": "2024-01-25T23:54:12.979Z"
},
"reason": "reply",
"reasonSubject": "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.post/3kp5qn72s232q",
"record": {
"$type": "app.bsky.feed.post",
"createdAt": "2024-04-02T14:09:18.926Z",
"langs": [
"ja"
],
"reply": {
"parent": {
"cid": "bafyreiewdfyh6rywpkdzpmf5markqa6tavc5smc32q7cw2wpwbqik5hnfm",
"uri": "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.post/3kp5qn72s232q"
},
"root": {
"cid": "bafyreiewdfyh6rywpkdzpmf5markqa6tavc5smc32q7cw2wpwbqik5hnfm",
"uri": "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.post/3kp5qn72s232q"
}
},
"text": "first"
},
"isRead": true,
"indexedAt": "2024-04-02T14:09:18.926Z",
"labels": []
}
```

37
scripts/setup-migration.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash
# ai.bot 移行セットアップスクリプト
echo "=== ai.bot Migration Setup ==="
# 1. 新しい設定ディレクトリの作成
echo "Creating new config directory..."
mkdir -p ~/.config/syui/ai/bot/
# 2. スクリプトディレクトリの移動gitサブモジュール
if [ -d ~/.config/ai/scpt ]; then
echo "Copying script directory..."
cp -r ~/.config/ai/scpt ~/.config/syui/ai/bot/
echo "Scripts copied to ~/.config/syui/ai/bot/scpt/"
fi
# 3. 設定ファイルの移行自動的にdata.rsで行われる
echo "Configuration files will be migrated automatically when used."
# 4. エイリアス設定の提案
echo ""
echo "=== Manual Steps Required ==="
echo ""
echo "1. Add this alias to your shell profile (~/.zshrc, ~/.bashrc, etc.):"
echo " alias ai='aibot'"
echo ""
echo "2. Install the new binaries:"
echo " cargo install --path ."
echo ""
echo "3. Or add to PATH:"
echo " export PATH=\"$(pwd)/target/debug:\$PATH\""
echo ""
echo "4. Update git submodule path if needed:"
echo " cd ~/.config/syui/ai/bot/scpt"
echo " git remote -v # Check current remote"
echo ""
echo "Migration setup complete!"

39
src/alias.rs Normal file
View File

@ -0,0 +1,39 @@
// Legacy alias: ai -> aibot
// This provides backward compatibility for existing scripts
use std::env;
use std::path::PathBuf;
use std::process::{Command, exit};
fn main() {
let args: Vec<String> = env::args().collect();
// Skip the first argument (program name) and pass the rest to aibot
let aibot_args: Vec<&str> = args.iter().skip(1).map(|s| s.as_str()).collect();
// Try to find aibot binary in the same directory as this binary
let current_exe = env::current_exe().unwrap_or_else(|_| PathBuf::from("ai"));
let mut aibot_path = current_exe.parent().unwrap_or(&PathBuf::from(".")).to_path_buf();
aibot_path.push("aibot");
// First try relative path, then try PATH
let mut cmd = if aibot_path.exists() {
Command::new(&aibot_path)
} else {
Command::new("aibot")
};
cmd.args(&aibot_args);
match cmd.status() {
Ok(status) => {
exit(status.code().unwrap_or(1));
}
Err(e) => {
eprintln!("Error executing aibot: {}", e);
eprintln!("Make sure 'aibot' is installed and available in PATH");
eprintln!("Attempted path: {:?}", aibot_path);
exit(1);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,34 +8,91 @@ use std::io::Write;
use std::path::Path;
pub fn data_file(s: &str) -> String {
let file = "/.config/ai/";
let mut f = shellexpand::tilde("~").to_string();
f.push_str(&file);
let path = Path::new(&f);
if path.is_dir() == false {
let _ = fs::create_dir_all(f.clone());
// 新しい設定ディレクトリ(優先)
let new_config_dir = "/.config/syui/ai/bot/";
let mut new_path = shellexpand::tilde("~").to_string();
new_path.push_str(&new_config_dir);
// 旧設定ディレクトリ(互換性のため)
let old_config_dir = "/.config/ai/";
let mut old_path = shellexpand::tilde("~").to_string();
old_path.push_str(&old_config_dir);
// 新しいディレクトリを作成
let new_dir = Path::new(&new_path);
if !new_dir.is_dir() {
let _ = fs::create_dir_all(new_path.clone());
}
match &*s {
"toml" => f + &"token.toml",
"json" => f + &"token.json",
"refresh" => f + &"refresh.toml",
_ => f + &"." + &s,
let filename = match &*s {
"toml" => "token.toml",
"json" => "token.json",
"refresh" => "refresh.toml",
_ => &format!(".{}", s),
};
let new_file = new_path.clone() + filename;
let old_file = old_path + filename;
// 新しいパスにファイルが存在する場合は新しいパスを使用
if Path::new(&new_file).exists() {
return new_file;
}
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行
if Path::new(&old_file).exists() && !Path::new(&new_file).exists() {
if let Ok(_) = fs::copy(&old_file, &new_file) {
eprintln!("Migrated config file: {} -> {}", old_file, new_file);
return new_file;
}
}
// デフォルトは新しいパス
new_file
}
pub fn log_file(s: &str) -> String {
let file = "/.config/ai/txt/";
let mut f = shellexpand::tilde("~").to_string();
f.push_str(&file);
let path = Path::new(&f);
if path.is_dir() == false {
let _ = fs::create_dir_all(f.clone());
// 新しい設定ディレクトリ(優先)
let new_log_dir = "/.config/syui/ai/bot/txt/";
let mut new_path = shellexpand::tilde("~").to_string();
new_path.push_str(&new_log_dir);
// 旧設定ディレクトリ(互換性のため)
let old_log_dir = "/.config/ai/txt/";
let mut old_path = shellexpand::tilde("~").to_string();
old_path.push_str(&old_log_dir);
// 新しいディレクトリを作成
let new_dir = Path::new(&new_path);
if !new_dir.is_dir() {
let _ = fs::create_dir_all(new_path.clone());
}
match &*s {
"n1" => f + &"notify_cid.txt",
"n2" => f + &"notify_cid_run.txt",
_ => f + &s,
let filename = match &*s {
"n1" => "notify_cid.txt",
"n2" => "notify_cid_run.txt",
"c1" => "comment_cid.txt",
_ => s,
};
let new_file = new_path.clone() + filename;
let old_file = old_path + filename;
// 新しいパスにファイルが存在する場合は新しいパスを使用
if Path::new(&new_file).exists() {
return new_file;
}
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行
if Path::new(&old_file).exists() && !Path::new(&new_file).exists() {
if let Ok(_) = fs::copy(&old_file, &new_file) {
eprintln!("Migrated log file: {} -> {}", old_file, new_file);
return new_file;
}
}
// デフォルトは新しいパス
new_file
}
impl Token {
@ -106,6 +163,7 @@ pub struct BaseUrl {
pub record_list: String,
pub record_create: String,
pub record_delete: String,
pub record_put: String,
pub session_create: String,
pub session_refresh: String,
pub session_get: String,
@ -123,6 +181,7 @@ pub struct BaseUrl {
pub follow: String,
pub follows: String,
pub followers: String,
pub feed_get: String,
}
pub fn url(s: &str) -> String {
@ -140,6 +199,7 @@ pub fn url(s: &str) -> String {
let baseurl = BaseUrl {
profile_get: "com.atproto.identity.resolveHandle".to_string(),
thread_get: "app.bsky.feed.getPostThread".to_string(),
record_put: "com.atproto.repo.putRecord".to_string(),
record_create: "com.atproto.repo.createRecord".to_string(),
record_delete: "com.atproto.repo.deleteRecord".to_string(),
describe: "com.atproto.repo.describeRepo".to_string(),
@ -148,6 +208,7 @@ pub fn url(s: &str) -> String {
session_refresh: "com.atproto.server.refreshSession".to_string(),
session_get: "com.atproto.server.getSession".to_string(),
timeline_get: "app.bsky.feed.getTimeline".to_string(),
feed_get: "app.bsky.feed.getFeed".to_string(),
timeline_author: "app.bsky.feed.getAuthorFeed".to_string(),
like: "app.bsky.feed.like".to_string(),
repost: "app.bsky.feed.repost".to_string(),
@ -170,6 +231,7 @@ pub fn url(s: &str) -> String {
"record_list" => t.to_string() + &baseurl.record_list,
"record_create" => t.to_string() + &baseurl.record_create,
"record_delete" => t.to_string() + &baseurl.record_delete,
"record_put" => t.to_string() + &baseurl.record_put,
"session_create" => t.to_string() + &baseurl.session_create,
"session_refresh" => t.to_string() + &baseurl.session_refresh,
"session_get" => t.to_string() + &baseurl.session_get,
@ -187,6 +249,7 @@ pub fn url(s: &str) -> String {
"follow" => t.to_string() + &baseurl.follow,
"follows" => t.to_string() + &baseurl.follows,
"followers" => t.to_string() + &baseurl.followers,
"feed_get" => t.to_string() + &baseurl.feed_get,
_ => s,
}
}

21
src/delete_record.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
use serde_json::json;
pub async fn post_request(rkey: String, col: String) -> String {
let handle = data_toml(&"handle");
let url = url(&"record_delete");
let client = HttpClient::new();
let post = json!({
"repo": handle.to_string(),
"rkey": rkey.to_string(),
"collection": col.to_string()
});
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,22 +1,13 @@
extern crate reqwest;
//use crate::data_toml;
use crate::http_client::HttpClient;
use crate::url;
pub async fn get_request(user: String) -> String {
//let token = data_refresh(&"access");
let url = url(&"describe");
let base_url = url(&"describe");
let url = format!("{}?repo={}", base_url, user);
let client = HttpClient::new();
let client = reqwest::Client::new();
let res = client
.get(url)
.query(&[("repo", &user)])
//.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
match client.get(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

13
src/feed_get.rs Normal file
View File

@ -0,0 +1,13 @@
use crate::http_client::HttpClient;
use crate::url;
pub async fn get_request(feed: String) -> String {
let base_url = url(&"feed_get");
let url = format!("{}?feed={}", base_url, feed);
let client = HttpClient::new();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(_) => "err".to_string(),
}
}

77
src/feed_watch.rs Normal file
View File

@ -0,0 +1,77 @@
use seahorse::Context;
//use crate::openai;
use crate::feed_get;
use crate::data::data_toml;
use crate::data::Timeline;
use crate::data::log_file;
use crate::data::w_cid;
pub fn c_feed_watch(c: &Context) {
let mut feed = "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd".to_string();
if c.string_flag("url").is_ok() {
feed = c.string_flag("url").unwrap();
}
let mut tag = "syai".to_string();
if c.string_flag("tag").is_ok() {
tag = c.string_flag("tag").unwrap();
}
let h = async {
let notify = feed_get::get_request(feed).await;
if notify == "err" {
return;
//refresh(c);
//notify = feed_get::get_request("at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd".to_string()).await;
}
let timeline: Timeline = serde_json::from_str(&notify).unwrap();
let n = timeline.feed;
let host = data_toml(&"host");
let length = &n.len();
let su = 0..*length;
for i in su {
let cid = &n[i].post.cid;
let check_cid = w_cid(cid.to_string(), log_file(&"n1"), false);
let handle = &n[i].post.author.handle;
let did = &n[i].post.author.did;
let uri = &n[i].post.uri;
let _time = &n[i].post.indexedAt;
let cid_root = cid;
let uri_root = uri;
let mut text = "";
if !n[i].post.record.text.is_none() {
text = &n[i].post.record.text.as_ref().unwrap();
}
let vec: Vec<&str> = text.split_whitespace().collect();
let com = vec[0].trim().to_string();
let mut prompt = "".to_string();
let mut prompt_sub = "".to_string();
if com == "@ai" || com == "/ai" || com == tag {
prompt_sub = vec[1..].join(" ");
} else {
prompt = vec[1..].join(" ");
if vec.len() > 1 {
prompt_sub = vec[2..].join(" ");
}
}
if check_cid == false && { prompt.is_empty() == false || com.is_empty() == false } {
println!("{}", handle);
if c.bool_flag("debug") == true {
println!(
"cid:{}\nuri:{}\ncid_root:{}\nuri_root:{}\nhost:{}\ndid:{}\ncheck_cid:{}",
cid, uri, cid_root, uri_root, host, did, check_cid
);
}
println!("{}", prompt_sub);
println!("---");
w_cid(cid.to_string(), log_file(&"n1"), true);
}
}
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res;
}

View File

@ -1,14 +1,12 @@
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use crate::http_client::HttpClient;
use iso8601_timestamp::Timestamp;
use serde_json::json;
//use crate::data::Follow;
pub async fn post_request(u: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -18,7 +16,7 @@ pub async fn post_request(u: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -26,25 +24,19 @@ pub async fn post_request(u: String) -> String {
"subject": u.to_string(),
"createdAt": d.to_string(),
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error following user: {}", e);
"err".to_string()
}
}
}
pub async fn delete_request(u: String, rkey: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -54,7 +46,7 @@ pub async fn delete_request(u: String, rkey: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -63,19 +55,14 @@ pub async fn delete_request(u: String, rkey: String) -> String {
"subject": u.to_string(),
"createdAt": d.to_string(),
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error unfollowing user: {}", e);
"err".to_string()
}
}
}

View File

@ -1,24 +1,14 @@
extern crate reqwest;
use crate::data_refresh;
use crate::http_client::HttpClient;
use crate::url;
//use serde_json::json;
pub async fn get_request(actor: String, cursor: Option<String>) -> String {
let token = data_refresh(&"access");
let url = url(&"followers");
let base_url = url(&"followers");
let cursor = cursor.unwrap();
let url = format!("{}?actor={}&cursor={}", base_url, actor, cursor);
let client = HttpClient::new();
let client = reqwest::Client::new();
let res = client
.get(url)
.query(&[("actor", actor), ("cursor", cursor)])
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,26 +1,14 @@
extern crate reqwest;
use crate::data_refresh;
use crate::http_client::HttpClient;
use crate::url;
//use serde_json::json;
pub async fn get_request(actor: String, cursor: Option<String>) -> String {
let token = data_refresh(&"access");
let url = url(&"follows");
let base_url = url(&"follows");
let cursor = cursor.unwrap();
//let cursor = "1682386039125::bafyreihwgwozmvqxcxrhbr65agcaa4v357p27ccrhzkjf3mz5xiozjvzfa".to_string();
//let cursor = "1682385956974::bafyreihivhux5m3sxbg33yruhw5ozhahwspnuqdsysbo57smzgptdcluem".to_string();
let url = format!("{}?actor={}&cursor={}", base_url, actor, cursor);
let client = HttpClient::new();
let client = reqwest::Client::new();
let res = client
.get(url)
.query(&[("actor", actor), ("cursor", cursor)])
//cursor.unwrap()
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

5
src/game.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod post_card;
pub mod post_card_verify;
pub mod post_game;
pub mod post_game_user;
pub mod post_game_login;

35
src/game/post_card.rs Normal file
View File

@ -0,0 +1,35 @@
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(verify: String, id: i32, cp: i32, rank: i32, rare: String, col: String, author: String) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
"record": {
"id": id,
"cp": cp,
"rank": rank,
"rare": rare.to_string(),
"author": author.to_string(),
"verify": verify.to_string(),
"createdAt": d.to_string(),
},
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -0,0 +1,49 @@
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(col: String, img: String, id: i32, cp: i32, rank: i32, rare: String, user_handle: String, user_did: String) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let d = Timestamp::now_utc();
let d = d.to_string();
let link = "https://bsky.app/profile/yui.syui.ai".to_string();
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
"record": {
"id": id,
"cp": cp,
"rank": rank,
"rare": rare.to_string(),
"handle": user_handle.to_string(),
"did": user_did.to_string(),
"embed": {
"$type": "app.bsky.embed.external",
"external": {
"uri": link,
"thumb": {
"$type": "blob",
"ref": {
"$link": img.to_string()
},
"mimeType": "image/jpeg",
"size": 0
}
}
},
"createdAt": d.to_string(),
},
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

30
src/game/post_game.rs Normal file
View File

@ -0,0 +1,30 @@
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(col: String, account: String) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_put");
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
"rkey": "self".to_string(),
"record": {
"account": account.to_string(),
"createdAt": d.to_string(),
},
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -0,0 +1,33 @@
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(col: String, username: String, login: bool, account: String) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_put");
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
"rkey": "self".to_string(),
"record": {
"login": login,
"username": username.to_string(),
"account": account.to_string(),
"createdAt": d.to_string(),
},
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -0,0 +1,46 @@
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(col: String, user_name: String, user_did: String, user_handle: String, aiten: i32, limit: i32, chara: String, lv: i32, exp: i32, hp: i32, rank: i32, mode: i32, attach: i32, critical: i32, critical_d: i32) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_put");
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
"rkey": user_name.to_string(),
"record": {
"did": user_did.to_string(),
"handle": user_handle.to_string(),
"aiten": aiten,
"limit": limit,
"character": {
chara.to_string(): {
"lv": lv,
"exp": exp,
"hp": hp,
"rank": rank,
"mode": mode,
"attach": attach,
"critical": critical,
"critical_d": critical_d,
}
},
"createdAt": d.to_string(),
"updatedAt": d.to_string(),
},
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

114
src/http_client.rs Normal file
View File

@ -0,0 +1,114 @@
use reqwest::{Client, Error};
use serde::Serialize;
use crate::data_refresh;
pub struct HttpClient {
client: Client,
}
impl HttpClient {
pub fn new() -> Self {
Self {
client: Client::new(),
}
}
/// GET request with authentication
pub async fn get_with_auth(&self, url: &str) -> Result<String, Error> {
let token = data_refresh(&"access");
let response = self.client
.get(url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await?
.text()
.await?;
Ok(response)
}
/// POST request with JSON body and authentication
pub async fn post_json_with_auth<T: Serialize>(&self, url: &str, json: &T) -> Result<String, Error> {
let token = data_refresh(&"access");
let response = self.client
.post(url)
.json(json)
.header("Authorization", format!("Bearer {}", token))
.send()
.await?
.text()
.await?;
Ok(response)
}
/// DELETE request with authentication
pub async fn delete_with_auth(&self, url: &str) -> Result<String, Error> {
let token = data_refresh(&"access");
let response = self.client
.delete(url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await?
.text()
.await?;
Ok(response)
}
/// POST request without authentication (for login, etc.)
pub async fn post_json<T: Serialize>(&self, url: &str, json: &T) -> Result<String, Error> {
let response = self.client
.post(url)
.json(json)
.send()
.await?
.text()
.await?;
Ok(response)
}
/// GET request without authentication
pub async fn get(&self, url: &str) -> Result<String, Error> {
let response = self.client
.get(url)
.send()
.await?
.text()
.await?;
Ok(response)
}
/// POST request with custom headers
pub async fn post_with_headers<T: Serialize>(
&self,
url: &str,
json: &T,
headers: Vec<(&str, &str)>
) -> Result<String, Error> {
let mut request = self.client.post(url).json(json);
for (key, value) in headers {
request = request.header(key, value);
}
let response = request
.send()
.await?
.text()
.await?;
Ok(response)
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,23 +1,19 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use serde_json::json;
use iso8601_timestamp::Timestamp;
pub async fn post_request(text: String, link: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -41,19 +37,12 @@ pub async fn post_request(text: String, link: String) -> String {
]
}
}
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,6 +1,5 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -12,17 +11,15 @@ pub async fn post_request(
uri: String,
itype: String,
) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -56,19 +53,12 @@ pub async fn post_request(
}
}
}
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,23 +1,19 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use serde_json::json;
use iso8601_timestamp::Timestamp;
pub async fn post_request(text: String, link: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -41,19 +37,12 @@ pub async fn post_request(text: String, link: String) -> String {
]
}
}
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,12 +1,10 @@
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use crate::http_client::HttpClient;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(cid: String, uri: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -16,7 +14,7 @@ pub async fn post_request(cid: String, uri: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -27,19 +25,14 @@ pub async fn post_request(cid: String, uri: String) -> String {
},
"createdAt": d.to_string(),
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error liking post: {}", e);
"err".to_string()
}
}
}

View File

@ -3,6 +3,7 @@ use std::env;
use crate::ascii::c_ascii;
use crate::bot::c_bot;
use crate::bot::c_bot_feed;
use crate::data::c_follow_all;
use crate::data::c_openai_key;
use crate::data::data_toml;
@ -10,6 +11,12 @@ use crate::data::data_refresh;
use crate::data::url;
use crate::data::w_cfg;
use crate::data::w_refresh;
use crate::feed_watch::c_feed_watch;
use crate::game::post_card;
use crate::game::post_card_verify;
use crate::game::post_game;
use crate::game::post_game_user;
use crate::game::post_game_login;
use data::ProfileIdentityResolve;
@ -20,6 +27,7 @@ pub mod describe;
pub mod follow;
pub mod followers;
pub mod follows;
pub mod http_client;
pub mod img_reply;
pub mod like;
pub mod mention;
@ -28,6 +36,7 @@ pub mod notify_read;
pub mod openai;
pub mod post;
pub mod post_link;
pub mod game;
pub mod profile;
pub mod refresh;
pub mod reply;
@ -37,10 +46,19 @@ pub mod repost;
pub mod session;
pub mod timeline_author;
pub mod token;
pub mod feed_get;
pub mod feed_watch;
pub mod delete_record;
#[cfg(test)]
mod tests;
fn main() {
let args: Vec<String> = env::args().collect();
let app = App::new(env!("CARGO_PKG_NAME"))
.author(env!("CARGO_PKG_AUTHORS"))
.version(env!("CARGO_PKG_VERSION"))
.description(env!("CARGO_PKG_DESCRIPTION"))
.command(
Command::new("ai")
.alias("a")
@ -59,6 +77,26 @@ fn main() {
Flag::new("admin", FlagType::String)
.alias("a"),
)
.flag(
Flag::new("feed", FlagType::String)
.alias("f"),
)
)
.command(
Command::new("feed_watch")
.action(feed_watch)
.flag(
Flag::new("url", FlagType::String)
.alias("u"),
)
.flag(
Flag::new("tag", FlagType::String)
.alias("t"),
)
.flag(
Flag::new("debug", FlagType::Bool)
.alias("d"),
)
)
.command(
Command::new("follow_all")
@ -95,6 +133,12 @@ fn main() {
.alias("t")
.action(timeline),
)
.command(
Command::new("feed")
.description("feed <feed-uri>")
.alias("f")
.action(feed)
)
.command(
Command::new("did")
.description("did <handle>")
@ -110,6 +154,170 @@ fn main() {
.alias("l"),
)
)
.command(
Command::new("delete")
.description("d <rkey> -c <collection>")
.alias("d")
.action(delete)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
)
.command(
Command::new("card")
.description("-v <at://verify> -i <int:id> -p <int:cp> -r <int:rank> -c <collection> -a <author> -img <link> -rare <normal>")
.action(card)
.flag(
Flag::new("id", FlagType::Int)
.alias("i"),
)
.flag(
Flag::new("cp", FlagType::Int)
.alias("p"),
)
.flag(
Flag::new("rank", FlagType::Int)
.alias("r"),
)
.flag(
Flag::new("rare", FlagType::Int)
)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
.flag(
Flag::new("author", FlagType::String)
.alias("a"),
)
.flag(
Flag::new("verify", FlagType::String)
.alias("v"),
)
.flag(
Flag::new("img", FlagType::String)
)
)
.command(
Command::new("card-verify")
.description("<at://verify> -c <collection> -i <id> -p <cp> -r <rank> -rare <normal> -H <syui.ai> -d <did>")
.action(card_verify)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
.flag(
Flag::new("id", FlagType::Int)
.alias("i"),
)
.flag(
Flag::new("cp", FlagType::Int)
.alias("p"),
)
.flag(
Flag::new("rank", FlagType::Int)
.alias("r"),
)
.flag(
Flag::new("rare", FlagType::String)
)
.flag(
Flag::new("handle", FlagType::String)
.alias("H"),
)
.flag(
Flag::new("did", FlagType::String)
.alias("did"),
)
)
.command(
Command::new("game")
.description("a <at://yui.syui.ai/ai.syui.game.user/username>")
.action(game)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
.flag(
Flag::new("account", FlagType::String)
.alias("a"),
)
)
.command(
Command::new("game-login")
.description("l <bool> -u <username> -c <collection>")
.action(game_login)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
.flag(
Flag::new("login", FlagType::Bool)
.alias("l"),
)
.flag(
Flag::new("username", FlagType::String)
.alias("u"),
)
)
.command(
Command::new("game-user")
.description("-chara ai -l 20240101 -ten 0 --lv 0 --exp 0 --hp 0 --rank 0 --mode 0 --attach 0 --critical 0 --critical_d 0")
.action(game_user)
.flag(
Flag::new("username", FlagType::String)
.alias("u"),
)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
.flag(
Flag::new("did", FlagType::String)
.alias("d"),
)
.flag(
Flag::new("handle", FlagType::String)
.alias("H"),
)
.flag(
Flag::new("character", FlagType::String)
.alias("chara"),
)
.flag(
Flag::new("aiten", FlagType::Int)
.alias("ten"),
)
.flag(
Flag::new("limit", FlagType::Int)
.alias("l"),
)
.flag(
Flag::new("lv", FlagType::Int)
)
.flag(
Flag::new("hp", FlagType::Int)
)
.flag(
Flag::new("attach", FlagType::Int)
)
.flag(
Flag::new("exp", FlagType::Int)
)
.flag(
Flag::new("critical", FlagType::Int)
)
.flag(
Flag::new("critical_d", FlagType::Int)
)
.flag(
Flag::new("rank", FlagType::Int)
)
.flag(
Flag::new("mode", FlagType::Int)
)
)
.command(
Command::new("like")
.description("like <cid> -u <uri>")
@ -190,6 +398,10 @@ fn main() {
Flag::new("post", FlagType::String)
.alias("p"),
)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
)
.command(
Command::new("follow")
@ -271,6 +483,14 @@ fn bot(c: &Context) {
refresh(c);
loop {
c_bot(c);
c_bot_feed(c);
}
}
fn feed_watch(c: &Context) {
refresh(c);
loop {
c_feed_watch(c);
}
}
@ -306,15 +526,12 @@ fn refresh(_c: &Context) {
let session = session::get_request().await;
if session == "err" {
let res = refresh::post_request().await;
println!("{}", res);
if res == "err" {
let m = data_toml(&"handle");
let p = data_toml(&"password");
let s = data_toml(&"host");
println!("handle:{}, pass:{}, host:{}", m, p, s);
let res = token::post_request(m.to_string(), p.to_string(), s.to_string()).await;
w_cfg(&s, &res, &p);
println!("res:{}", res);
} else {
w_refresh(&res);
}
@ -334,6 +551,22 @@ fn notify(c: &Context) {
return res;
}
fn feed(c: &Context) {
refresh(c);
let feed_d = "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd".to_string();
let h = async {
if c.args.len() == 0 {
let j = feed_get::get_request(feed_d).await;
println!("{}", j);
} else {
let j = feed_get::get_request(c.args[0].to_string()).await;
println!("{}", j);
}
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res;
}
fn did(c: &Context) {
refresh(c);
let h = async {
@ -383,6 +616,19 @@ fn post(c: &Context) {
return res;
}
fn delete(c: &Context) {
refresh(c);
let m = c.args[0].to_string();
let h = async {
if let Ok(col) = c.string_flag("col") {
let str = delete_record::post_request(m.to_string(), col);
println!("{}", str.await);
}
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res;
}
fn like(c: &Context) {
refresh(c);
let m = c.args[0].to_string();
@ -396,6 +642,145 @@ fn like(c: &Context) {
return res;
}
async fn c_card(c: &Context) -> Result<(), Box<dyn std::error::Error>> {
//let m = c.args[0].to_string();
let author = c.string_flag("author").unwrap_or_else(|_| "syui".to_string());
let verify = c.string_flag("verify").unwrap_or_else(|_| "at://did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.card.verify/3lagpvhppmd2q".to_string());
let col = c.string_flag("col").unwrap_or_else(|_| "ai.syui.card".to_string());
//let img = c.string_flag("img").unwrap_or_else(|_| "bafkreigvcjc46qtelpc4wsg7fwf6qktbi6a23ouqiupth2r37zhrn7wbza".to_string());
let id = c.int_flag("id")?.try_into()?;
let cp = c.int_flag("cp")?.try_into()?;
let rank = c.int_flag("rank")?.try_into()?;
let rare = c.string_flag("rare").unwrap_or_else(|_| "normal".to_string());
let str = post_card::post_request(verify, id, cp, rank, rare, col, author);
println!("{}", str.await);
Ok(())
}
fn card(c: &Context) {
refresh(c);
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
if let Err(e) = c_card(c).await {
eprintln!("Error: {}", e);
}
});
}
async fn c_card_verify(c: &Context) -> Result<(), Box<dyn std::error::Error>> {
let col = c.string_flag("col").unwrap_or_else(|_| "ai.syui.card.verify".to_string());
let img = c.string_flag("img").unwrap_or_else(|_| "bafkreigvcjc46qtelpc4wsg7fwf6qktbi6a23ouqiupth2r37zhrn7wbza".to_string());
let id = c.int_flag("id")?.try_into()?;
let cp = c.int_flag("cp")?.try_into()?;
let rank = c.int_flag("rank")?.try_into()?;
let rare = c.string_flag("rare").unwrap_or_else(|_| "normal".to_string());
let user_handle = c.string_flag("handle").unwrap_or_else(|_| "syui.ai".to_string());
let user_did = c.string_flag("did").unwrap_or_else(|_| "did:plc:uqzpqmrjnptsxezjx4xuh2mn".to_string());
//match id === 1 let img = "xxx";
let str = post_card_verify::post_request(col, img, id, cp, rank, rare, user_handle, user_did);
println!("{}", str.await);
Ok(())
}
fn card_verify(c: &Context) {
refresh(c);
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
if let Err(e) = c_card_verify(c).await {
eprintln!("Error: {}", e);
}
});
}
async fn c_game(c: &Context) -> Result<(), Box<dyn std::error::Error>> {
let account = c.string_flag("account").unwrap_or_else(|_| "at://did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.game.user/syui".to_string());
let col = c.string_flag("col").unwrap_or_else(|_| "ai.syui.game".to_string());
let handle = data_toml(&"handle");
if handle == "syui.ai" {
let str = post_game::post_request(col, account);
println!("{}", str.await);
Ok(())
} else {
Err(Box::new(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Not authorized")))
}
}
fn game(c: &Context) {
refresh(c);
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
if let Err(e) = c_game(c).await {
eprintln!("Error: {}", e);
}
});
}
async fn c_game_user(c: &Context) -> Result<(), Box<dyn std::error::Error>> {
let col = c.string_flag("col").unwrap_or_else(|_| "ai.syui.game.user".to_string());
let user_name = c.string_flag("username").unwrap_or_else(|_| "syui".to_string());
let user_handle = c.string_flag("handle").unwrap_or_else(|_| "syui.ai".to_string());
let user_did = c.string_flag("did").unwrap_or_else(|_| "did:plc:uqzpqmrjnptsxezjx4xuh2mn".to_string());
let chara = c.string_flag("character").unwrap_or_else(|_| "ai".to_string());
let limit = c.int_flag("limit")?.try_into()?;
let aiten = c.int_flag("aiten")?.try_into()?;
let lv = c.int_flag("lv")?.try_into()?;
let exp = c.int_flag("exp")?.try_into()?;
let hp = c.int_flag("hp")?.try_into()?;
let rank = c.int_flag("rank")?.try_into()?;
let mode = c.int_flag("mode")?.try_into()?;
let attach = c.int_flag("attach")?.try_into()?;
let critical = c.int_flag("critical")?.try_into()?;
let critical_d = c.int_flag("critical_d")?.try_into()?;
if data_toml(&"handle") == "yui.syui.ai" {
let str = post_game_user::post_request(col, user_name, user_did, user_handle, aiten, limit, chara, lv, exp, hp, rank, mode, attach, critical, critical_d);
println!("{}", str.await);
Ok(())
} else {
Err(Box::new(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Not authorized")))
}
}
fn game_user(c: &Context) {
refresh(c);
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
if let Err(e) = c_game_user(c).await {
eprintln!("Error: {}", e);
}
});
}
async fn c_game_login(c: &Context) -> Result<(), Box<dyn std::error::Error>> {
let col = c.string_flag("col").unwrap_or_else(|_| "ai.syui.game.login".to_string());
let user_name = c.string_flag("username").unwrap_or_else(|_| "syui".to_string());
let account = "at://did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.game.user/".to_string() + &user_name;
let login = c.bool_flag("login");
if data_toml(&"handle") == "yui.syui.ai" {
let str = post_game_login::post_request(col, user_name, login, account);
println!("{}", str.await);
Ok(())
} else {
Err(Box::new(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Not authorized")))
}
}
fn game_login(c: &Context) {
refresh(c);
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
if let Err(e) = c_game_login(c).await {
eprintln!("Error: {}", e);
}
});
}
fn repost(c: &Context) {
refresh(c);
let m = c.args[0].to_string();
@ -450,6 +835,7 @@ fn mention(c: &Context) {
let h = async {
let str = profile::get_request(m.to_string()).await;
let profile: ProfileIdentityResolve = serde_json::from_str(&str).unwrap();
let col = c.string_flag("col").unwrap_or_else(|_| "app.bsky.feed.post".to_string());
let udid = profile.did;
let handle = m.to_string();
let at = "@".to_owned() + &handle;
@ -457,6 +843,7 @@ fn mention(c: &Context) {
let s = 0;
if let Ok(post) = c.string_flag("post") {
let str = mention::post_request(
col,
post.to_string(),
at.to_string(),
udid.to_string(),

View File

@ -1,28 +1,24 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(text: String, at: String, udid: String, s: i32, e: i32) -> String {
let token = data_refresh(&"access");
pub async fn post_request(col: String, text: String, at: String, udid: String, s: i32, e: i32) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"did": did.to_string(),
"repo": handle.to_string(),
"collection": col.to_string(),
"record": {
"text": at.to_string() + &" ".to_string() + &text.to_string(),
"$type": "app.bsky.feed.post",
"$type": col.to_string(),
"createdAt": d.to_string(),
"facets": [
{
@ -39,19 +35,12 @@ pub async fn post_request(text: String, at: String, udid: String, s: i32, e: i32
}
]
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,30 +1,13 @@
extern crate reqwest;
use crate::data_refresh;
use crate::http_client::HttpClient;
use crate::url;
//use serde_json::json;
pub async fn get_request(limit: i32) -> String {
let token = data_refresh(&"access");
let url = url(&"notify_list");
let base_url = url(&"notify_list");
let url = format!("{}?limit={}", base_url, limit);
let client = HttpClient::new();
let client = reqwest::Client::new();
let res = client
.get(url)
.query(&[("limit", limit)])
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap();
let status_ref = res.error_for_status_ref();
match status_ref {
Ok(_) => {
return res.text().await.unwrap();
}
Err(_e) => {
let e = "err".to_string();
return e;
}
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(_) => "err".to_string(),
}
}

View File

@ -1,27 +1,17 @@
extern crate reqwest;
use crate::data_refresh;
use crate::http_client::HttpClient;
use crate::url;
use serde_json::json;
pub async fn post_request(time: String) -> String {
let token = data_refresh(&"access");
let url = url(&"notify_update");
let client = HttpClient::new();
let post = Some(json!({
let post = json!({
"seenAt": time.to_string(),
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -33,7 +33,7 @@ pub async fn post_request(prompt: String) -> String {
";
let post = Some(json!({
"model": "gpt-3.5-turbo",
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": &setting.to_string()},
{"role": "user", "content": &prompt.to_string()},

View File

@ -1,12 +1,10 @@
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use crate::http_client::HttpClient;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(text: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -16,7 +14,7 @@ pub async fn post_request(text: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -24,19 +22,14 @@ pub async fn post_request(text: String) -> String {
"text": text.to_string(),
"createdAt": d.to_string(),
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error posting: {}", e);
"err".to_string()
}
}
}

View File

@ -1,22 +1,19 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(text: String, link: String, s: i32, e: i32) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -38,19 +35,12 @@ pub async fn post_request(text: String, link: String, s: i32, e: i32) -> String
}
],
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,21 +1,15 @@
extern crate reqwest;
use crate::data_refresh;
use crate::url;
use crate::http_client::HttpClient;
pub async fn get_request(user: String) -> String {
let token = data_refresh(&"access");
let url = url(&"profile_get") + &"?handle=" + &user;
let client = reqwest::Client::new();
let res = client
.get(url)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error getting profile: {}", e);
"err".to_string()
}
}
}

View File

@ -1,28 +1,18 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::url;
pub async fn post_request() -> String {
let refresh = data_toml(&"refresh");
let url = url(&"session_refresh");
let client = HttpClient::new();
let client = reqwest::Client::new();
let res = client
.post(url)
.header("Authorization", "Bearer ".to_owned() + &refresh)
.send()
.await
.unwrap();
let status_ref = res.error_for_status_ref();
match status_ref {
Ok(_) => {
return res.text().await.unwrap();
}
Err(_e) => {
let e = "err".to_string();
return e;
}
let auth_header = format!("Bearer {}", refresh);
let headers = vec![("Authorization", auth_header.as_str())];
let empty_json = serde_json::json!({});
match client.post_with_headers(&url, &empty_json, headers).await {
Ok(response) => response,
Err(_) => "err".to_string(),
}
}

View File

@ -1,7 +1,6 @@
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use crate::http_client::HttpClient;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -12,7 +11,6 @@ pub async fn post_request(
cid_root: String,
uri_root: String,
) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -23,7 +21,7 @@ pub async fn post_request(
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -41,19 +39,14 @@ pub async fn post_request(
}
}
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error replying to post: {}", e);
"err".to_string()
}
}
}

View File

@ -1,6 +1,5 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -15,17 +14,15 @@ pub async fn post_request(
cid_root: String,
uri_root: String,
) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -57,19 +54,12 @@ pub async fn post_request(
}
],
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,6 +1,5 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -16,17 +15,15 @@ pub async fn post_request(
title: String,
description: String,
) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -60,19 +57,12 @@ pub async fn post_request(
}
}
}
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,12 +1,10 @@
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use crate::http_client::HttpClient;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(cid: String, uri: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -16,7 +14,7 @@ pub async fn post_request(cid: String, uri: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
let post = json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -27,19 +25,14 @@ pub async fn post_request(cid: String, uri: String) -> String {
},
"createdAt": d.to_string(),
},
}));
});
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error reposting: {}", e);
"err".to_string()
}
}
}

View File

@ -1,28 +1,12 @@
extern crate reqwest;
use crate::data_refresh;
use crate::http_client::HttpClient;
use crate::url;
pub async fn get_request() -> String {
let token = data_refresh(&"access");
let url = url(&"session_get");
let client = HttpClient::new();
let client = reqwest::Client::new();
let res = client
.get(url)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap();
let status_ref = res.error_for_status_ref();
match status_ref {
Ok(_) => {
return res.text().await.unwrap();
}
Err(_e) => {
let e = "err".to_string();
return e;
}
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(_) => "err".to_string(),
}
}

View File

@ -0,0 +1,24 @@
use crate::http_client::HttpClient;
#[test]
fn test_http_client_creation() {
let _client = HttpClient::new();
// HttpClientが正しく作成されることを確認
let _client2 = HttpClient::default();
}
#[tokio::test]
async fn test_http_client_error_handling() {
let client = HttpClient::new();
// 無効なURLでエラーが返ることを確認
let result = client.get("http://invalid-url-that-does-not-exist.local").await;
assert!(result.is_err());
}
// モジュールが正しくコンパイルされることを確認
#[test]
fn test_module_imports() {
// モジュールが存在することを確認
assert!(true);
}

2
src/tests/mod.rs Normal file
View File

@ -0,0 +1,2 @@
#[cfg(test)]
mod http_client_tests;

View File

@ -1,27 +1,14 @@
extern crate reqwest;
use crate::data_refresh;
use crate::http_client::HttpClient;
use crate::url;
pub async fn get_request(actor: String) -> String {
let token = data_refresh(&"access");
let url = url(&"record_list");
let actor = actor.to_string();
//let cursor = cursor.unwrap();
let base_url = url(&"record_list");
let col = "app.bsky.feed.post".to_string();
let client = reqwest::Client::new();
let res = client
.get(url)
.query(&[("repo", actor), ("collection", col)])
//.query(&[("actor", actor),("cursor", cursor)])
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
let url = format!("{}?repo={}&collection={}", base_url, actor, col);
let client = HttpClient::new();
return res;
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -1,25 +1,17 @@
extern crate reqwest;
use crate::http_client::HttpClient;
use std::collections::HashMap;
pub async fn post_request(handle: String, pass: String, host: String) -> String {
let url = "https://".to_owned()
+ &host.to_string()
+ &"/xrpc/com.atproto.server.createSession".to_string();
let url = format!("https://{}/xrpc/com.atproto.server.createSession", host);
let mut map = HashMap::new();
map.insert("identifier", &handle);
map.insert("password", &pass);
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&map)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
let client = HttpClient::new();
match client.post_json(&url, &map).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
}

View File

@ -9,13 +9,14 @@ case $OSTYPE in
esac
d=${0:a:h}
source $d/env
source $d/env.zsh
source $d/refresh.zsh
source $d/token.zsh
source $d/reply.zsh
source $d/notify.zsh
source $d/notify_cid.zsh
source $d/cron.zsh
source $d/feed.zsh
case $1 in
refresh|r)
@ -36,4 +37,7 @@ case $1 in
cid)
cid
;;
feed)
feed
;;
esac

View File

@ -1,7 +1,7 @@
function cron() {
t=`docker ps |grep aios|grep Up`
t=`docker ps |grep aios|grep R`
if [ -z "$t" ];then
docker compose up -d
exit
fi
exit
docker compose up -d
}

View File

@ -1,3 +1,4 @@
#!/bin/zsh
ai l $HANDLE -p $PASSWORD -s $HOST && ai bot -a $ADMIN
#ai l $HANDLE -p $PASSWORD -s $HOST
ai bot -a $ADMIN

5
test/feed.zsh Normal file
View File

@ -0,0 +1,5 @@
function feed(){
token=`cat ~/.config/ai/token.json|jq -r .accessJwt`
url=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd
curl -sL "https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=$url" -H "Authorization: Bearer $token"
}