Bluesky Pythonでポスト検索などあれこれ AT Protocol SDK

# 話題のSNS Bluesky

若者のX離れ…のせいかわかりませんが、新たなSNSの**Bluesky**が人気になっています。
PythonでのAPIの利用についてポストは[こちら](https://qiita.com/ynack/items/85ccf951727f8ac48c6c)や[こちら](https://qiita.com/koyukitukimino/items/ff0f75e86fc7cf7a5124)の記事が参考になったのですがポストの検索などその他の処理の記載がなかったのでご参考までに紹介します。

基本的には以下のドキュメント通りです。

https://atproto.blue/en/latest/

https://docs.bsky.app/docs/

※下の方はメソッド名が若干異なっているので注意です

## 用語について

- <b>ハンドル名…Xなどで言うユーザ名で"～.bsky.social"などです。
- <b>DID…アカウント固有のID。ハンドル名ではなく変えることのできない値で"did:～"で始まります。
- <b>URI…投稿（ポスト）を識別するID。Xと違いこのIDには上記DIDが含まれるようです。
- <b>CID…ポストのコンテンツハッシュ値。ポストの正当性を保証。 ※[こちらから参照しました](https://docs.bsky.app/docs/tutorials/creating-a-post)</b>

以下で**Actor**という場合はハンドル名とDIDどちらでも使えるときに利用します

# インストール・ログイン・ポスト
```python:AT Protocol SDKインストール
pip install atproto
```

```python:ログイン
from atproto import Client

client = Client()
client.login("ハンドル名（*****.bsky.socialなど）", "パスワード")
```
```python:ポスト（send_post）
post_text = "ポストテスト"

client.send_post(text=post_text)
```

# プロフィール取得

get_profile(**Actor**)でアカウントのプロフィールを取得します[→ドキュメント](https://atproto.blue/en/latest/atproto_client/client.html#atproto_client.client.client.Client.get_profile)

```python:get_profile
profile = client.get_profile("sora-sakurai.bsky.social")
print(profile)
```

```:出力結果
did='did:plc:nc45m3kcywjhyr4tgsbmea64' handle='sora-sakurai.bsky.social' associated=ProfileAssociated(chat=None, feedgens=0, labeler=False, lists=0, starter_packs=0, py_type='app.bsky.actor.defs#profileAssociated') avatar='https://cdn.bsky.app/img/avatar/plain/did:plc:nc45m3kcywjhyr4tgsbmea64/bafkreigfxce42hajzeutuziv6o4fogryn7rtrlxmlni2vgoinidqcdti44@jpeg' banner=None created_at='2024-02-06T16:17:56.807Z' description='ソラ(Sora)代表 ゲームデザイナー/ゲームディレクター \n『星のカービィ』『大乱闘スマッシュブラザーズ』など｡ \nとりあえずアカウント作ってみました!' display_name='桜井 政博 / Masahiro Sakurai' followers_count=61309 follows_count=0 indexed_at='2024-02-09T12:25:28.465Z' joined_via_starter_pack=None labels=[] pinned_post=None posts_count=63 viewer=ViewerState(blocked_by=False, blocking=None, blocking_by_list=None, followed_by=None, following=None, known_followers=None, muted=False, muted_by_list=None, py_type='app.bsky.actor.defs#viewerState') py_type='app.bsky.actor.defs#profileViewDetailed'
```

profile.display_nameなどで[それぞれの情報](https://atproto.blue/en/latest/atproto/atproto_client.models.app.bsky.actor.defs.html#atproto_client.models.app.bsky.actor.defs.ProfileViewDetailed)を取得できます。
アカウントのDIDを知りたい場合もprofile.didで取得して下さい。
（先述の通りActorなのでDIDからでも取得できます。）

※ちなみに自分のプロフィールはclient.me.XXXXXで取得できます。

# フォロー・フォロワー取得
get_follows(Actor), get_followers(Actor)で対象のアカウントのフォロー・フォロワーを取得できます。[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto.html#atproto.Client.get_followers)

```python:フォローアカウント取得
res = client.get_follows("unicouniuni3.bsky.social")

for profile in res.follows:
    print(profile.handle, profile.display_name)
```
```:出力結果
kusagamegunso.bsky.social クサガメ軍曹（アメチカンのもな）
monamofumofu.bsky.social もなつむ
potene.bsky.social ぽてねのアポロとチロル兄さん
```

```python:フォロワー取得
res = client.get_followers("unicouniuni3.bsky.social")

for profile in res.followers:
    print(profile.handle, profile.display_name)
```
```:出力結果
yorunosoko.bsky.social ヨノ底
randomcolor.bsky.social Lotus Nocturne
cozirase-ushio.bsky.social ジャーキー
```

# ユーザ検索
app.bsky.actor.search_actorsでアカウントの検索ができます。[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto_client.models.app.bsky.actor.search_actors.html#module-atproto_client.models.app.bsky.actor.search_actors)

```python:
actors = client.app.bsky.actor.search_actors({"q":"Vtuber", "limit":25})

for actor in actors.actors:
    print(actor.handle, actor.display_name, actor.description)
```
```:出力結果
akakura1341.bsky.social 赤倉🧸 イラストレーター🖋 甘いものとファッションとレトロが好きです
(…省略)
```

必須なのは検索ワードとなる<b>"q"</b>のみで最大件数などそれ以外は任意です。
これまでのメソッドと違い、引数を辞書型でセットします。
**※client.app.bsky.XXXXX系のメソッドは引数を辞書型などで指定する必要があるようです**

# フォロー
follows(did)で対象のdidのアカウントをフォローします。[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto.html#atproto.Client.follow)
これはハンドル名は利用できませんので、search_actorsなどでDIDを取得して下さい。

```python:
res = client.app.bsky.actor.search_actors({"q":"Vtuber", "limit":1})

for actor in res.actors:
    
    client.follow(actor.did)
```
※一気にフォローしてしまうので注意してください

フォロー解除の[unfollow](https://atproto.blue/en/latest/atproto/atproto.html#atproto.Client.unfollow)も同様です。

# ポスト検索
search_postsでポストの検索ができます。必須なのは検索ワードの<b>"q"</b>だけです。[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto_client.models.app.bsky.feed.search_posts.html)

```python:search_posts
res = client.app.bsky.feed.search_posts({"q":"地震", "sort":"top", "limit":5})

for post in res.posts:
    print(post.author.display_name, post.record.text)
```
```:出力結果
ウェザーニュース 【海外地震情報】
日本時間の11月15日(金)14時29分頃、海外で地震がありました。
震源地はニューギニア付近（パプアニューギニア、ニューアイルランド）で、地震の規模はM6.7と推定されます。この地震による津波被害の心配はありません。
(…省略)
```

ソート（sort）は未指定なら最新順、"top"は見られている順になるかと思います。

対象のアカウントだけのポストを取得したい場合"author"にハンドル名をセットすればいいのですが、"q"が必須項目で面倒なので"q"に<b>"from:(ハンドル名)"</b>を指定した方が楽かと思います。

```python:search_posts（ユーザ指定）
res = client.app.bsky.feed.search_posts({"q":"from:unicouniuni3.bsky.social"})
```
<br>
他にもアプリで使うように検索クエリが"q"にも利用できるかと思います。

https://scrapbox.io/Bluesky/%E6%A4%9C%E7%B4%A2%E3%82%AF%E3%82%A8%E3%83%AA


# いいね・リポスト
likeとrepostでいいね・リポストができます。ただXと違うのはポストのIDだけではなくCIDも必要になります。→ドキュメント[いいね](https://atproto.blue/en/latest/atproto_client/client.html#atproto_client.client.client.Client.like)・[リポスト](https://atproto.blue/en/latest/atproto_client/client.html#atproto_client.client.client.Client.repost)


```python:
res = client.app.bsky.feed.search_posts({"q":"from:unicouniuni3.bsky.social", "limit":1})

for post in res.posts:

    client.like(post.uri, post.cid)
    client.repost(post.uri, post.cid)
```

こちらも一気に操作できてしまうので注意して下さい。

ちなみにURIが

**"at://did:plc:5ysft3tkkncrn3xohxwqybxl/app.bsky.feed.post/3lba2gm3gys24"**

だとして、ポストを表示するにはat://→https://bsky.app/profile/ にして"app.bsky.feed."を消して

**https://bsky.app/profile/did:plc:5ysft3tkkncrn3xohxwqybxl/post/3lba2gm3gys24**

ポストが表示できるようになります。（DIDをハンドルに変えても大丈夫です）

# 返信
返信は面倒です。send_postで返信先を指定するのですがReplyRefとやらにしなくてはいけません。[→ドキュメント](https://atproto.blue/en/latest/atproto_client/client.html#atproto_client.client.client.Client.send_post)

**①元ポストへの返信の場合（返信への返信でない）**

```python:返信するポストのuriとcidを取得
res = client.app.bsky.feed.search_posts({"q":"from:nekoniiii.com", "limit":1})
uri=res.posts[0].uri
cid=res.posts[0].cid
```

```python:ReplyRefをセット
from atproto import models

model = models.base.ModelBase(uri=uri, cid=cid)

parent = models.utils.create_strong_ref(model)

# parentとrootは同じ
root = parent

reply_ref=models.AppBskyFeedPost.ReplyRef(parent=parent, root=root)
```
```python:返信実行
create_record = client.send_post(text='返信',reply_to=reply_ref)
```

<br>

**②返信への返信の場合（①の返信に返信します）**

```python:ReplyRefをセット
# ①の返信で得たuri・cidをセット
model = models.base.ModelBase(uri=create_record.uri, cid=create_record.cid)

parent = models.utils.create_strong_ref(model)

# rootは①と同じ
reply_ref=models.AppBskyFeedPost.ReplyRef(parent=parent, root=root)
```
```python:返信実行
create_record2 = client.send_post(text='返信への返信', reply_to=reply_ref)
```

<br>

ややこしいですがparentに返信するポスト、rootに元となるポストをセットします。[こちらも参考にして下さい。](https://docs.bsky.app/docs/tutorials/creating-a-post)

# その他

### ブロック一覧（自身がブロックしているアカウントです）
[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto_client.models.app.bsky.graph.get_blocks.html#module-atproto_client.models.app.bsky.graph.get_blocks)

```python:
res = client.app.bsky.graph.get_blocks()

for profile in res.blocks:
    print(profile.handle, profile.display_name)
```
```:出力結果
matthewstadlen.bsky.social Matthew Stadlen
(…省略)
```

### ポストへいいねしたアカウント一覧
[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto.html#atproto.Client.get_likes)


```python:
res = client.get_likes(uri="at://did:plc:sme75d7sju4ttl2ew3bexf6c/app.bsky.feed.post/3lbmfnufn2s2c")

for like in res.likes:
    print(like.actor.handle, like.actor.display_name)
```
```:出力結果
matthewstadlen.bsky.social
(…省略)
```

### 自分がいいねしたポスト一覧
[→ドキュメント](https://atproto.blue/en/latest/atproto/atproto_client.models.app.bsky.feed.get_actor_likes.html#module-atproto_client.models.app.bsky.feed.get_actor_likes)

```python:
res = client.app.bsky.feed.get_actor_likes({"actor":"nekoniiii.com"})

for feed in res.feed:
    print(feed.post.author.handle, feed.post.author.display_name, feed.post.record.text)
```
```:出力結果
kcns.bsky.social 梔 最近の猫
(…省略)
```

<br>
"get_actor_likes"という名前から任意のユーザのいいねが見れるかと思いきや、自分のしか見れません。（他のユーザーはエラーになります）

<b>「今後解禁する…？」</b>のかもしれません。

# おわりに

XのAPIと比べて利用までが大変だったり、直感的にわかりづらい点が多い気もしました。（特にURIですぐにポストが確認出来ないのが面倒でした…）
理解できてない部分も大きいのでこれから学びたいと思います。
紹介できたのはほんの一部の機能ですが、ご参考にして下さい。