Compare commits

..

2 Commits

Author SHA1 Message Date
454de01881 add lexicons
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2024-11-09 09:51:05 +09:00
57365a41a5 add card 2024-11-09 09:51:05 +09:00
67 changed files with 1358 additions and 2218 deletions

View File

@ -1,22 +0,0 @@
{
"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

@ -1,17 +1,9 @@
[package]
name = "aibot"
name = "ai"
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"
description = "latest@2024-08-18"
[dependencies]
seahorse = "*"
@ -27,7 +19,3 @@ rustc-serialize = "*"
toml = "*"
iso8601-timestamp = "*"
sysinfo = "*"
[dev-dependencies]
mockito = "1.2"
tokio-test = "0.4"

View File

@ -1,7 +1,9 @@
FROM syui/aios
ADD .ssh /root/.ssh
WORKDIR /root
ADD ./test/entrypoint.sh .
RUN chmod +x /root/entrypoint.sh
RUN pacman -Syu bc --noconfirm
ENTRYPOINT ["/root/entrypoint.sh"]

View File

@ -17,14 +17,6 @@ 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

@ -108,7 +108,7 @@ $ docker compose build
$ docker compose up -d
```
## pds:card
## pds
- https://atproto.com/ja/guides/lexicon
- https://at.syu.is/at/did:plc:uqzpqmrjnptsxezjx4xuh2mn/ai.syui.card/3lagpwihqxi2v
@ -116,29 +116,4 @@ $ docker compose up -d
```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
```

View File

@ -0,0 +1,56 @@
{
"lexicon": 1,
"id": "ai.syui.card",
"defs": {
"main": {
"type": "record",
"description": "Record containing a cards box.",
"key": "tid",
"record": {
"type": "object",
"required": ["verify", "createdAt"],
"properties": {
"id":{
"type": "integer",
"minimum": 0,
"maximum": 14,
"default": 0
},
"cp":{
"type": "integer",
"minimum": 1,
"maximum": 5000,
"default": 1
},
"rank":{
"type": "integer",
"minimum": 0,
"maximum": 7,
"default": 0
},
"rare": {
"type": "string",
"enum": ["normal", "super", "ultra", "yui", "ai"],
"default": "normal"
},
"author": {
"type": "string",
"format": "uri",
"description": "https://verify...",
"default": "https://yui.syui.ai"
},
"verify": {
"type": "string",
"format": "at-uri",
"description": "at://verify..."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this post was originally created."
}
}
}
}
}
}

View File

@ -0,0 +1,62 @@
{
"lexicon": 1,
"id": "ai.syui.card.verify",
"defs": {
"main": {
"type": "record",
"description": "Record containing a card verify.",
"key": "tid",
"record": {
"type": "object",
"required": ["handle", "did", "createdAt"],
"properties": {
"id":{
"type": "integer",
"minimum": 0,
"maximum": 14,
"default": 0
},
"cp":{
"type": "integer",
"minimum": 1,
"maximum": 10000,
"default": 1
},
"rank":{
"type": "integer",
"minimum": 0,
"maximum": 7,
"default": 0
},
"rare": {
"type": "string",
"enum": ["normal", "super", "ultra", "yui", "ai"],
"default": "normal"
},
"handle": {
"type": "string",
"maxLength": 32,
"maxGraphemes": 32
},
"did": {
"type": "string"
},
"embed": {
"type": "union",
"refs": [
"app.bsky.embed.images",
"app.bsky.embed.external",
"app.bsky.embed.record",
"app.bsky.embed.recordWithMedia"
]
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this post was originally created."
}
}
}
}
}
}

View File

@ -0,0 +1,30 @@
{
"lexicon": 1,
"id": "ai.syui.o.comment",
"defs": {
"main": {
"type": "record",
"description": "Record containing a Frontpage comment.",
"key": "tid",
"record": {
"type": "object",
"required": ["content", "createdAt", "post"],
"properties": {
"content": {
"type": "string",
"maxLength": 100000,
"maxGraphemes": 10000,
"description": "The content of the comment."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this comment was originally created."
},
"parent": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
"post": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
}
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"lexicon": 1,
"id": "ai.syui.o.post",
"defs": {
"main": {
"type": "record",
"description": "Record containing a Frontpage post.",
"key": "tid",
"record": {
"type": "object",
"required": ["title", "url", "createdAt"],
"properties": {
"title": {
"type": "string",
"maxLength": 3000,
"maxGraphemes": 300,
"description": "The title of the post."
},
"url": {
"type": "string",
"format": "uri",
"description": "The URL of the post."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this post was originally created."
}
}
}
}
}
}

View File

@ -0,0 +1,23 @@
{
"lexicon": 1,
"id": "ai.syui.o.vote",
"defs": {
"main": {
"type": "record",
"description": "Record containing a Frontpage vote.",
"key": "tid",
"record": {
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this vote was originally created."
}
}
}
}
}
}

View File

@ -0,0 +1,126 @@
{
"lexicon": 1,
"id": "com.atproto.repo.applyWrites",
"defs": {
"main": {
"type": "procedure",
"description": "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["repo", "writes"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo (aka, current account)."
},
"validate": {
"type": "boolean",
"description": "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons."
},
"writes": {
"type": "array",
"items": {
"type": "union",
"refs": ["#create", "#update", "#delete"],
"closed": true
}
},
"swapCommit": {
"type": "string",
"description": "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.",
"format": "cid"
}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": [],
"properties": {
"commit": {
"type": "ref",
"ref": "com.atproto.repo.defs#commitMeta"
},
"results": {
"type": "array",
"items": {
"type": "union",
"refs": ["#createResult", "#updateResult", "#deleteResult"],
"closed": true
}
}
}
}
},
"errors": [
{
"name": "InvalidSwap",
"description": "Indicates that the 'swapCommit' parameter did not match current commit."
}
]
},
"create": {
"type": "object",
"description": "Operation which creates a new record.",
"required": ["collection", "value"],
"properties": {
"collection": { "type": "string", "format": "nsid" },
"rkey": { "type": "string", "maxLength": 15 },
"value": { "type": "unknown" }
}
},
"update": {
"type": "object",
"description": "Operation which updates an existing record.",
"required": ["collection", "rkey", "value"],
"properties": {
"collection": { "type": "string", "format": "nsid" },
"rkey": { "type": "string" },
"value": { "type": "unknown" }
}
},
"delete": {
"type": "object",
"description": "Operation which deletes an existing record.",
"required": ["collection", "rkey"],
"properties": {
"collection": { "type": "string", "format": "nsid" },
"rkey": { "type": "string" }
}
},
"createResult": {
"type": "object",
"required": ["uri", "cid"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"validationStatus": {
"type": "string",
"knownValues": ["valid", "unknown"]
}
}
},
"updateResult": {
"type": "object",
"required": ["uri", "cid"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"validationStatus": {
"type": "string",
"knownValues": ["valid", "unknown"]
}
}
},
"deleteResult": {
"type": "object",
"required": [],
"properties": {}
}
}
}

View File

@ -0,0 +1,72 @@
{
"lexicon": 1,
"id": "com.atproto.repo.createRecord",
"defs": {
"main": {
"type": "procedure",
"description": "Create a single new repository record. Requires auth, implemented by PDS.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["repo", "collection", "record"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo (aka, current account)."
},
"collection": {
"type": "string",
"format": "nsid",
"description": "The NSID of the record collection."
},
"rkey": {
"type": "string",
"description": "The Record Key.",
"maxLength": 15
},
"validate": {
"type": "boolean",
"description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons."
},
"record": {
"type": "unknown",
"description": "The record itself. Must contain a $type field."
},
"swapCommit": {
"type": "string",
"format": "cid",
"description": "Compare and swap with the previous commit by CID."
}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["uri", "cid"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"commit": {
"type": "ref",
"ref": "com.atproto.repo.defs#commitMeta"
},
"validationStatus": {
"type": "string",
"knownValues": ["valid", "unknown"]
}
}
}
},
"errors": [
{
"name": "InvalidSwap",
"description": "Indicates that 'swapCommit' didn't match current repo commit."
}
]
}
}
}

View File

@ -0,0 +1,14 @@
{
"lexicon": 1,
"id": "com.atproto.repo.defs",
"defs": {
"commitMeta": {
"type": "object",
"required": ["cid", "rev"],
"properties": {
"cid": { "type": "string", "format": "cid" },
"rev": { "type": "string" }
}
}
}
}

View File

@ -0,0 +1,56 @@
{
"lexicon": 1,
"id": "com.atproto.repo.deleteRecord",
"defs": {
"main": {
"type": "procedure",
"description": "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["repo", "collection", "rkey"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo (aka, current account)."
},
"collection": {
"type": "string",
"format": "nsid",
"description": "The NSID of the record collection."
},
"rkey": {
"type": "string",
"description": "The Record Key."
},
"swapRecord": {
"type": "string",
"format": "cid",
"description": "Compare and swap with the previous record by CID."
},
"swapCommit": {
"type": "string",
"format": "cid",
"description": "Compare and swap with the previous commit by CID."
}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"properties": {
"commit": {
"type": "ref",
"ref": "com.atproto.repo.defs#commitMeta"
}
}
}
},
"errors": [{ "name": "InvalidSwap" }]
}
}
}

View File

@ -0,0 +1,51 @@
{
"lexicon": 1,
"id": "com.atproto.repo.describeRepo",
"defs": {
"main": {
"type": "query",
"description": "Get information about an account and repository, including the list of collections. Does not require auth.",
"parameters": {
"type": "params",
"required": ["repo"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo."
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": [
"handle",
"did",
"didDoc",
"collections",
"handleIsCorrect"
],
"properties": {
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" },
"didDoc": {
"type": "unknown",
"description": "The complete DID document for this account."
},
"collections": {
"type": "array",
"description": "List of all the collections (NSIDs) for which this repo contains at least one record.",
"items": { "type": "string", "format": "nsid" }
},
"handleIsCorrect": {
"type": "boolean",
"description": "Indicates if handle is currently valid (resolves bi-directionally)"
}
}
}
}
}
}
}

View File

@ -0,0 +1,45 @@
{
"lexicon": 1,
"id": "com.atproto.repo.getRecord",
"defs": {
"main": {
"type": "query",
"description": "Get a single record from a repository. Does not require auth.",
"parameters": {
"type": "params",
"required": ["repo", "collection", "rkey"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo."
},
"collection": {
"type": "string",
"format": "nsid",
"description": "The NSID of the record collection."
},
"rkey": { "type": "string", "description": "The Record Key." },
"cid": {
"type": "string",
"format": "cid",
"description": "The CID of the version of the record. If not specified, then return the most recent version."
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["uri", "value"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"value": { "type": "unknown" }
}
}
},
"errors": [{ "name": "RecordNotFound" }]
}
}
}

View File

@ -0,0 +1,13 @@
{
"lexicon": 1,
"id": "com.atproto.repo.importRepo",
"defs": {
"main": {
"type": "procedure",
"description": "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.",
"input": {
"encoding": "application/vnd.ipld.car"
}
}
}
}

View File

@ -0,0 +1,44 @@
{
"lexicon": 1,
"id": "com.atproto.repo.listMissingBlobs",
"defs": {
"main": {
"type": "query",
"description": "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.",
"parameters": {
"type": "params",
"properties": {
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"default": 500
},
"cursor": { "type": "string" }
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["blobs"],
"properties": {
"cursor": { "type": "string" },
"blobs": {
"type": "array",
"items": { "type": "ref", "ref": "#recordBlob" }
}
}
}
}
},
"recordBlob": {
"type": "object",
"required": ["cid", "recordUri"],
"properties": {
"cid": { "type": "string", "format": "cid" },
"recordUri": { "type": "string", "format": "at-uri" }
}
}
}
}

View File

@ -0,0 +1,69 @@
{
"lexicon": 1,
"id": "com.atproto.repo.listRecords",
"defs": {
"main": {
"type": "query",
"description": "List a range of records in a repository, matching a specific collection. Does not require auth.",
"parameters": {
"type": "params",
"required": ["repo", "collection"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo."
},
"collection": {
"type": "string",
"format": "nsid",
"description": "The NSID of the record type."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 50,
"description": "The number of records to return."
},
"cursor": { "type": "string" },
"rkeyStart": {
"type": "string",
"description": "DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)"
},
"rkeyEnd": {
"type": "string",
"description": "DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)"
},
"reverse": {
"type": "boolean",
"description": "Flag to reverse the order of the returned records."
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["records"],
"properties": {
"cursor": { "type": "string" },
"records": {
"type": "array",
"items": { "type": "ref", "ref": "#record" }
}
}
}
}
},
"record": {
"type": "object",
"required": ["uri", "cid", "value"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"value": { "type": "unknown" }
}
}
}
}

View File

@ -0,0 +1,73 @@
{
"lexicon": 1,
"id": "com.atproto.repo.putRecord",
"defs": {
"main": {
"type": "procedure",
"description": "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["repo", "collection", "rkey", "record"],
"nullable": ["swapRecord"],
"properties": {
"repo": {
"type": "string",
"format": "at-identifier",
"description": "The handle or DID of the repo (aka, current account)."
},
"collection": {
"type": "string",
"format": "nsid",
"description": "The NSID of the record collection."
},
"rkey": {
"type": "string",
"description": "The Record Key.",
"maxLength": 15
},
"validate": {
"type": "boolean",
"description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons."
},
"record": {
"type": "unknown",
"description": "The record to write."
},
"swapRecord": {
"type": "string",
"format": "cid",
"description": "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation"
},
"swapCommit": {
"type": "string",
"format": "cid",
"description": "Compare and swap with the previous commit by CID."
}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["uri", "cid"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"commit": {
"type": "ref",
"ref": "com.atproto.repo.defs#commitMeta"
},
"validationStatus": {
"type": "string",
"knownValues": ["valid", "unknown"]
}
}
}
},
"errors": [{ "name": "InvalidSwap" }]
}
}
}

View File

@ -0,0 +1,15 @@
{
"lexicon": 1,
"id": "com.atproto.repo.strongRef",
"description": "A URI with a content-hash fingerprint.",
"defs": {
"main": {
"type": "object",
"required": ["uri", "cid"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" }
}
}
}
}

View File

@ -0,0 +1,23 @@
{
"lexicon": 1,
"id": "com.atproto.repo.uploadBlob",
"defs": {
"main": {
"type": "procedure",
"description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.",
"input": {
"encoding": "*/*"
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["blob"],
"properties": {
"blob": { "type": "blob" }
}
}
}
}
}
}

View File

@ -1,117 +0,0 @@
# 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)の「コントリビューション」章を参照してください。

View File

@ -1,332 +0,0 @@
# 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/)

View File

@ -1,334 +0,0 @@
# 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`文に置換
- [ ] エラーメッセージの追加

View File

@ -1,271 +0,0 @@
# 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)

View File

@ -1,208 +0,0 @@
# 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,37 +0,0 @@
#!/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!"

View File

@ -1,39 +0,0 @@
// 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);
}
}
}

View File

@ -431,42 +431,6 @@ pub fn c_bot(c: &Context) {
println!("{}", str_rep);
w_cid(cid.to_string(), log_file(&"n1"), true);
}
} else if com == "game" || com == "/game" {
let output = Command::new(data_scpt(&"ai"))
.arg(&"atproto").arg(&"game")
.arg(&handle)
.arg(&did)
.arg(&cid)
.arg(&uri)
.arg(&cid_root)
.arg(&uri_root)
.arg(&host)
.arg(&prompt)
.arg(&prompt_sub)
.output()
.expect("zsh");
let d = String::from_utf8_lossy(&output.stdout);
let dd = "\n".to_owned() + &d.to_string();
let text_limit = c_char(dd);
handlev = d.lines().collect::<Vec<_>>()[0].to_string();
link = "https://card.syui.ai/".to_owned() + &handlev;
println!("{}", e);
e = link.chars().count();
if text_limit.len() > 3 {
let str_rep = reply_link::post_request(
text_limit.to_string(),
link.to_string(),
s,
e.try_into().unwrap(),
cid.to_string(),
uri.to_string(),
cid_root.to_string(),
uri_root.to_string(),
)
.await;
println!("{}", str_rep);
w_cid(cid.to_string(), log_file(&"n1"), true);
}
} else if com == "quiz" || com == "/quiz" {
println!("admin:{}", admin);
let output = Command::new(data_scpt(&"ai"))

View File

@ -8,91 +8,35 @@ use std::io::Write;
use std::path::Path;
pub fn data_file(s: &str) -> String {
// 新しい設定ディレクトリ(優先)
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());
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 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;
match &*s {
"toml" => f + &"token.toml",
"json" => f + &"token.json",
"refresh" => f + &"refresh.toml",
_ => f + &"." + &s,
}
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行
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 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());
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 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;
match &*s {
"n1" => f + &"notify_cid.txt",
"n2" => f + &"notify_cid_run.txt",
"c1" => f + &"comment_cid.txt",
_ => f + &s,
}
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行
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 {
@ -163,7 +107,6 @@ 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,
@ -199,7 +142,6 @@ 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(),
@ -231,7 +173,6 @@ 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,

View File

@ -1,21 +1,33 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use serde_json::json;
pub async fn post_request(rkey: String, col: String) -> String {
let token = data_refresh(&"access");
//let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_delete");
let client = HttpClient::new();
let post = json!({
let url = url(&"record_delete");
let post = Some(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),
}
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;
}

View File

@ -1,13 +1,22 @@
use crate::http_client::HttpClient;
extern crate reqwest;
//use crate::data_toml;
use crate::url;
pub async fn get_request(user: String) -> String {
let base_url = url(&"describe");
let url = format!("{}?repo={}", base_url, user);
let client = HttpClient::new();
//let token = data_refresh(&"access");
let url = url(&"describe");
match client.get(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,13 +1,33 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
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();
let token = data_refresh(&"access");
let url = url(&"feed_get");
let feed = feed.to_string();
//let col = "app.bsky.feed.generator".to_string();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(_) => "err".to_string(),
let client = reqwest::Client::new();
let res = client
.get(url)
.query(&[("feed", feed)])
//.query(&[("feed", feed), ("collection", col)])
.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;
}
}
}

View File

@ -1,12 +1,14 @@
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");
@ -16,7 +18,7 @@ pub async fn post_request(u: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -24,19 +26,25 @@ pub async fn post_request(u: String) -> String {
"subject": u.to_string(),
"createdAt": d.to_string(),
},
});
}));
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()
}
}
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;
}
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");
@ -46,7 +54,7 @@ pub async fn delete_request(u: String, rkey: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -55,14 +63,19 @@ pub async fn delete_request(u: String, rkey: String) -> String {
"subject": u.to_string(),
"createdAt": d.to_string(),
},
});
}));
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()
}
}
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;
}

View File

@ -1,14 +1,24 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
use crate::url;
//use serde_json::json;
pub async fn get_request(actor: String, cursor: Option<String>) -> String {
let base_url = url(&"followers");
let token = data_refresh(&"access");
let url = url(&"followers");
let cursor = cursor.unwrap();
let url = format!("{}?actor={}&cursor={}", base_url, actor, cursor);
let client = HttpClient::new();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,14 +1,26 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
use crate::url;
//use serde_json::json;
pub async fn get_request(actor: String, cursor: Option<String>) -> String {
let base_url = url(&"follows");
let token = data_refresh(&"access");
let url = url(&"follows");
let cursor = cursor.unwrap();
let url = format!("{}?actor={}&cursor={}", base_url, actor, cursor);
let client = HttpClient::new();
//let cursor = "1682386039125::bafyreihwgwozmvqxcxrhbr65agcaa4v357p27ccrhzkjf3mz5xiozjvzfa".to_string();
//let cursor = "1682385956974::bafyreihivhux5m3sxbg33yruhw5ozhahwspnuqdsysbo57smzgptdcluem".to_string();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

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

View File

@ -1,30 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,46 +0,0 @@
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),
}
}

View File

@ -1,114 +0,0 @@
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,19 +1,23 @@
use crate::http_client::HttpClient;
extern crate reqwest;
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 = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -37,12 +41,19 @@ pub async fn post_request(text: String, link: String) -> String {
]
}
}
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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
}

View File

@ -1,5 +1,6 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -11,15 +12,17 @@ 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 = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -53,12 +56,19 @@ pub async fn post_request(
}
}
}
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,19 +1,23 @@
use crate::http_client::HttpClient;
extern crate reqwest;
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 = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -37,12 +41,19 @@ pub async fn post_request(text: String, link: String) -> String {
]
}
}
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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
}

View File

@ -1,10 +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;
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");
@ -14,7 +16,7 @@ pub async fn post_request(cid: String, uri: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -25,14 +27,19 @@ pub async fn post_request(cid: String, uri: String) -> String {
},
"createdAt": d.to_string(),
},
});
}));
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()
}
}
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;
}

View File

@ -12,11 +12,6 @@ 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;
@ -27,7 +22,6 @@ 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;
@ -36,7 +30,8 @@ pub mod notify_read;
pub mod openai;
pub mod post;
pub mod post_link;
pub mod game;
pub mod post_card;
pub mod post_card_verify;
pub mod profile;
pub mod refresh;
pub mod reply;
@ -50,9 +45,6 @@ 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"))
@ -201,7 +193,7 @@ fn main() {
)
.command(
Command::new("card-verify")
.description("<at://verify> -c <collection> -i <id> -p <cp> -r <rank> -rare <normal> -H <syui.ai> -d <did>")
.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)
@ -224,100 +216,13 @@ fn main() {
)
.flag(
Flag::new("handle", FlagType::String)
.alias("H"),
.alias("handle"),
)
.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>")
@ -398,10 +303,6 @@ fn main() {
Flag::new("post", FlagType::String)
.alias("p"),
)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
)
.command(
Command::new("follow")
@ -677,8 +578,6 @@ async fn c_card_verify(c: &Context) -> Result<(), Box<dyn std::error::Error>> {
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(())
@ -695,92 +594,6 @@ fn card_verify(c: &Context) {
});
}
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();
@ -835,7 +648,6 @@ 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;
@ -843,7 +655,6 @@ 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,24 +1,28 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
pub async fn post_request(col: String, text: String, at: String, udid: String, s: i32, e: i32) -> String {
pub async fn post_request(text: String, at: String, udid: 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 = json!({
let post = Some(json!({
"did": did.to_string(),
"repo": handle.to_string(),
"collection": col.to_string(),
"record": {
"text": at.to_string() + &" ".to_string() + &text.to_string(),
"$type": col.to_string(),
"$type": "app.bsky.feed.post",
"createdAt": d.to_string(),
"facets": [
{
@ -35,12 +39,19 @@ pub async fn post_request(col: String, text: String, at: String, udid: String, s
}
]
},
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,13 +1,30 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
use crate::url;
//use serde_json::json;
pub async fn get_request(limit: i32) -> String {
let base_url = url(&"notify_list");
let url = format!("{}?limit={}", base_url, limit);
let client = HttpClient::new();
let token = data_refresh(&"access");
let url = url(&"notify_list");
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(_) => "err".to_string(),
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;
}
}
}

View File

@ -1,17 +1,27 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
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 = json!({
let post = Some(json!({
"seenAt": time.to_string(),
});
}));
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,10 +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;
pub async fn post_request(text: String) -> String {
let token = data_refresh(&"access");
let did = data_toml(&"did");
let handle = data_toml(&"handle");
@ -14,7 +16,7 @@ pub async fn post_request(text: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -22,14 +24,19 @@ pub async fn post_request(text: String) -> String {
"text": text.to_string(),
"createdAt": d.to_string(),
},
});
}));
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()
}
}
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;
}

View File

@ -1,17 +1,21 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
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 token = data_refresh(&"access");
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!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -24,12 +28,19 @@ pub async fn post_request(verify: String, id: i32, cp: i32, rank: i32, rare: Str
"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),
}
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;
}

View File

@ -1,17 +1,19 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
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 token = data_refresh(&"access");
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!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -38,12 +40,19 @@ pub async fn post_request(col: String, img: String, id: i32, cp: i32, rank: i32,
},
"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),
}
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;
}

View File

@ -1,19 +1,22 @@
use crate::http_client::HttpClient;
extern crate reqwest;
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 = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -35,12 +38,19 @@ pub async fn post_request(text: String, link: String, s: i32, e: i32) -> String
}
],
},
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,15 +1,21 @@
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 = HttpClient::new();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error getting profile: {}", e);
"err".to_string()
}
}
let client = reqwest::Client::new();
let res = client
.get(url)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
}

View File

@ -1,18 +1,28 @@
use crate::http_client::HttpClient;
extern crate reqwest;
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 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(),
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;
}
}
}

View File

@ -1,6 +1,7 @@
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;
@ -11,6 +12,7 @@ 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");
@ -21,7 +23,7 @@ pub async fn post_request(
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -39,14 +41,19 @@ pub async fn post_request(
}
}
},
});
}));
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()
}
}
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;
}

View File

@ -1,5 +1,6 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -14,15 +15,17 @@ 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 = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -54,12 +57,19 @@ pub async fn post_request(
}
],
},
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,5 +1,6 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use iso8601_timestamp::Timestamp;
use serde_json::json;
@ -15,15 +16,17 @@ 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 = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -57,12 +60,19 @@ pub async fn post_request(
}
}
}
});
}));
let client = HttpClient::new();
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
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;
}

View File

@ -1,10 +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;
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");
@ -14,7 +16,7 @@ pub async fn post_request(cid: String, uri: String) -> String {
let d = Timestamp::now_utc();
let d = d.to_string();
let post = json!({
let post = Some(json!({
"repo": handle.to_string(),
"did": did.to_string(),
"collection": col.to_string(),
@ -25,14 +27,19 @@ pub async fn post_request(cid: String, uri: String) -> String {
},
"createdAt": d.to_string(),
},
});
}));
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()
}
}
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;
}

View File

@ -1,12 +1,28 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
use crate::url;
pub async fn get_request() -> String {
let token = data_refresh(&"access");
let url = url(&"session_get");
let client = HttpClient::new();
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(_) => "err".to_string(),
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;
}
}
}

View File

@ -1,24 +0,0 @@
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);
}

View File

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

View File

@ -1,14 +1,27 @@
use crate::http_client::HttpClient;
extern crate reqwest;
use crate::data_refresh;
use crate::url;
pub async fn get_request(actor: String) -> String {
let base_url = url(&"record_list");
let col = "app.bsky.feed.post".to_string();
let url = format!("{}?repo={}&collection={}", base_url, actor, col);
let client = HttpClient::new();
let token = data_refresh(&"access");
let url = url(&"record_list");
match client.get_with_auth(&url).await {
Ok(response) => response,
Err(e) => format!("Error: {}", e),
}
let actor = actor.to_string();
//let cursor = cursor.unwrap();
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();
return res;
}

View File

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

View File

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