+++ date = "2024-04-02" tags = ["bluesky", "atproto"] title = "blueskyのコメントシステムを作ってみた" slug = "bluesky" +++ この前、blueskyで特定の投稿に返信することで反映されるコメントシステムを作ってみました。 どういうふうに実現しているのかというと、結構複雑ですが、簡単に説明すると、私は以前からbotを動かしていて、そのついでに返信くらいは取得することができるので、新しく`openapi`を追加してそこにコメント情報を登録し、このapiから取得する情報でwebページを生成します。 ![](/img/bluesky_comment_system_0001.png) - https://manga.syui.ai 簡単に概要を見ていくとこんな感じ。 ```rust:src/main.rs if uri_root == &manga_uri { println!("manga_uri:{}", manga_uri); let output = Command::new(data_scpt(&"ai")) .arg(&"atproto").arg(&"manga") .arg(&handle) .arg(&did) .arg(&cid) .arg(&uri) .arg(&cid_root) .arg(&uri_root) .arg(&host) .arg(&avatar) .arg(&prompt_chat) .output() .expect("zsh"); let d = String::from_utf8_lossy(&output.stdout); let d = d.to_string(); let text_limit = c_char(d); let str_rep = reply::post_request( text_limit.to_string(), 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); } ``` ```sh:src/manga.zsh function manga_text() { repo=$did collection=app.bsky.feed.post url="https://$host/xrpc/com.atproto.repo.getRecord?repo=$repo&collection=$collection&rkey=$rkey&cid=$cid" export text=`curl -sL $url|jq -r .value.text` } function manga_add() { aid=2 api=https://api.syui.ai avatar=$com_option text=$com_option_sub_all export rkey=`echo $uri|cut -d / -f 5` bsky_url="https://bsky.app/profile/$did/post/$rkey" if [ "$host" = "syu.is" ];then bsky_url="https://web.syu.is/profile/$did/post/$rkey" fi manga_text j="{\"owner\":$aid, \"password\":\"$pass\"}" export mid=`curl -X POST -H "Content-Type: application/json" -d $j -sL $api/mas|jq -r .id` j="{\"updated_at\":\"$date_iso\", \"token\":\"$token\", \"did\":\"$did\", \"cid\":\"$cid\", \"uri\":\"$uri\", \"rkey\":\"$rkey\", \"bsky_url\":\"$bsky_url\", \"avatar\":\"$avatar\", \"handle\":\"$handle\", \"text\": \"$text\"}" tmp=`curl -X PATCH -H "Content-Type: application/json" -d $j -sL $api/mas/$mid` echo thx } ``` ```json:openapi.json "/mas": { "get": { "tags": [ "Ma" ], "summary": "List Mas", "description": "List Mas.", "operationId": "listMa", "parameters": [ { "name": "page", "in": "query", "description": "what page to render", "schema": { "type": "integer", "minimum": 1 } }, { "name": "itemsPerPage", "in": "query", "description": "item count to render per page", "schema": { "type": "integer", "maximum": 5000, "minimum": 1 } } ], "responses": { "200": { "description": "result Ma list", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/MaList" } } } } }, "400": { "$ref": "#/components/responses/400" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "500": { "$ref": "#/components/responses/500" } } }, "post": { "tags": [ "Ma" ], "summary": "Create a new Ma", "description": "Creates a new Ma and persists it to storage.", "operationId": "createMa", "requestBody": { "description": "Ma to create", "content": { "application/json": { "schema": { "type": "object", "properties": { "password": { "type": "string" }, "token": { "type": "string" }, "limit": { "type": "boolean" }, "count": { "type": "integer" }, "handle": { "type": "string" }, "text": { "type": "string" }, "did": { "type": "string" }, "avatar": { "type": "string" }, "cid": { "type": "string" }, "uri": { "type": "string" }, "rkey": { "type": "string" }, "bsky_url": { "type": "string" }, "updated_at": { "type": "string", "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" }, "owner": { "type": "integer" } }, "required": [ "password", "owner" ] } } }, "required": true }, "responses": { "200": { "description": "Ma created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MaCreate" } } } }, "400": { "$ref": "#/components/responses/400" }, "409": { "$ref": "#/components/responses/409" }, "500": { "$ref": "#/components/responses/500" } } } } ``` ```html:src/App.vue

{{ moment(comment_first.updated_at) }} @{{ comment_first.handle }} {{ comment_first.text }}

post

{{ axios_check('/icon/' + i.did.replace('did:plc:', '') + '.jpg') }} {{ moment(i.updated_at) }} @{{ i.handle }} {{ i.text }}

``` blueskyのapiは、いくつか認証不要のものがありますが、それではavatarとかreplyとかthreadとかを取れません。したがって、loginする必要があります。 また、avatarはリンク切れを起こす可能性が非常に高いので、downloadしたものを参照する必要があります。これはactivitypubとかと同じですね。 axiosでlocal-fileを確認して、それがない場合のみapi-linkを使用します。 ```js:src/App.vue methods: { axios_check(url) { axios.get(url) .catch(error => { this.url_check = false; }); } } ``` 次に削除に対応する方法ですが、これにはいくつか手段があります。ただし、どちらも非常に負荷が高いものになります。前者は都度確認する方法、後者は定期的に確認する方法です。前者のほうが遅延、負荷が高く、後者は緩やかです。ですが、その処理は後者のほうが面倒になります。 前者は`com.atproto.repo.getRecord`を叩いて投稿が存在すれば表示します。 後者は定期的に`com.atproto.repo.getRecord`を叩いて削除されているものをコメントから一括削除します。 ```js:src/manga.zsh host=bsky.social handle=yui.syui.ai collection=app.bsky.feed.post rkey=3kp2uq5kgns2k cid=bafyreibytb3lnpuyus24fpib6eb3nbmjlqb2hfrztlxuygsuznpngmty3u curl -sL "${host}/xrpc/com.atproto.repo.getRecord?repo=$handle&collection=$collection&rkey=$rkey&cid=$cid" ``` したがって、基本的には一度投稿されたものは表示したままにしておくか、数ヶ月に一度、cleanupするのがいいでしょう。