1
0
hugo/content/blog/2024-04-02-bluesky.md
2024-04-23 22:21:26 +09:00

301 lines
11 KiB
Markdown

+++
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
<div class="bsky_comment" v-if="comment_open == false">
<span class="comment">
<p class="comment-body" v-if="comment_first">
<img :src="'/icon/' + comment_first.did.replace('did:plc:', '') + '.jpg'" v-if="comment_first.avatar" class="comment"/> <span class="comment-time" v-if="comment_first.updated_at"><a :href="comment_first.bsky_url">{{ moment(comment_first.updated_at) }}</a></span> <span class="comment-handle" v-if="comment_first.handle"><a :href="'https://' + comment_first.bsky_url.split('/').slice(2,5).join('/')">@{{ comment_first.handle }}</a></span>
<span class="comment-text" v-if="comment_first.text">{{ comment_first.text }}</span>
</p>
</span>
<div class="comment_open">
<p>
<a :href="comment_first.bsky_url" target="_blank">post</a>
</p>
<p>
<button class="comment_open" v-on:click="comment_open = !comment_open"><i class="fa-solid fa-chevron-down"></i></button>
</p>
</div>
</div>
<div class="bsky_comment" v-else>
<span v-for="i in api_json.data" class="comment">
<p class="comment-body" v-if="i">
{{ axios_check('/icon/' + i.did.replace('did:plc:', '') + '.jpg') }}
<img :src="'/icon/' + i.did.replace('did:plc:', '') + '.jpg'" v-if="url_check" class="comment"/><img :src="i.avatar" v-else-if="i.avatar" class="comment"/> <span class="comment-time" v-if="i.updated_at"><a :href="i.bsky_url">{{ moment(i.updated_at) }}</a></span> <span class="comment-handle" v-if="i.handle"><a :href="'https://' + i.bsky_url.split('/').slice(2,5).join('/')">@{{ i.handle }}</a></span>
<span class="comment-text" v-if="i.text">{{ i.text }}</span>
</p>
</span>
<div class="comment_open"><button class="comment_open" v-on:click="comment_open = !comment_open"><i class="fa-solid fa-chevron-up"></i></button></div>
</div>
```
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するのがいいでしょう。