GitHub Actionsで定期実行しながらリモートリポに貯めていって、気が向いたらpullしてローカルmemosに取り込むか、そこまで細かく収集しないで手動でコマンド打ったら新着30件を取得するかで悩む。。。。後者でいい気もしてる。。
APIを取得するやり方がわかりやすい記事:
公式:
取得するデータの様子を観察する。
import os
import json
from dotenv import load_dotenv
from atproto import Client
# .envファイルの内容を読み込む
load_dotenv()
def fetch_user_timeline(username, password):
client = Client(base_url='https://bsky.social')
client.login(username, password)
response = client.get_timeline(cursor='', limit=30) # タイムラインを取得
return response.json() # レスポンスをJSON形式に変換して返す
if __name__ == "__main__":
username = os.getenv("BLUESKY_USERNAME")
password = os.getenv("BLUESKY_PASSWORD")
try:
timeline = fetch_user_timeline(username, password)
print("Fetched timeline:", timeline)
# 取得したデータを整形して保存
with open('timeline_data.json', 'w', encoding='utf-8') as f:
json.dump(json.loads(timeline), f, ensure_ascii=False, indent=4)
print("Timeline data saved to timeline_data.json")
except Exception as e:
print("Error:", e)
↑このファイルを実行すると
こんな感じで保存できるようにした。
これで、どこを取っていけばいいか把握できる💡
import os
import json
from dotenv import load_dotenv
from atproto import Client
# .envファイルの内容を読み込む
load_dotenv()
def fetch_author_posts(username, password, author_did):
client = Client(base_url='https://bsky.social')
client.login(username, password)
data = client.get_author_feed(
actor=author_did,
filter='posts_and_author_threads',
limit=30
)
return data.json() # レスポンスをJSON形式に変換して返す
if __name__ == "__main__":
username = os.getenv("BLUESKY_USERNAME")
password = os.getenv("BLUESKY_PASSWORD")
author_did = os.getenv("BLUESKY_AUTHOR_DID") # .envからのDIDの読み込み
try:
author_posts = fetch_author_posts(username, password, author_did)
print("Fetched author posts:", author_posts)
# 取得したデータを整形して保存
with open('author_posts_data.json', 'w', encoding='utf-8') as f:
json.dump(json.loads(author_posts), f, ensure_ascii=False, indent=4) # ここでindentを指定
print("Author posts data saved to author_posts_data.json")
except Exception as e:
print("Error:", e)
↑このファイルを実行すると
こんな感じで保存できるようにした。
コンテナ内部に入る
docker exec -it memos /bin/sh
データベースファイルのディレクトリに移動
cd /var/opt/memos
sqliteがインストールされていない場合は入れる
apk add --no-cache sqlite
SQLiteクライアントを起動
sqlite3 memos_prod.db
dbのテーブル一覧表示
.tables
↓こうなる
/var/opt/memos # sqlite3 memos_prod.db
SQLite version 3.44.2 2023-11-24 11:41:44
Enter ".help" for usage hints.
sqlite> .tables
activity memo_organizer resource user_setting
idp memo_relation storage webhook
inbox migration_history system_setting
memo reaction user
memoテーブルのデータを確認
SELECT * FROM memo;
↓
テーブルの値が文字列になってターミナルにどどど〜〜っと返ってくる。
1|77hR6LNcuKRNTkaffqc9tD|1|1716018699|1716018699|NORMAL|ターミナルから作業する:
- dify
- memos
さて、memosのデータはせめてobsidianに集結させた方がいいのでは。。。?
サーバー使ってないからショートカットからは飛ばせないだろうし。。。
前後関係をみながらメモするならPCで十分でもあるよな〜|PRIVATE|[]|{"property":{}}
2|PZSUdcRnBeMYk5u8pvvN9J|1|1716025194|1716025194|NORMAL|よし!リハしよう|PRIVATE|[]|{"property":{}}
3|45x8QyCMK9iEbzvWLMY4pe|1|1716036699|1716036699|NORMAL|感動。。。うまく言葉にできないけどちゃんとした形にして3chブログには投稿したい|PRIVATE|[]|{"property":{}}
4|PVRZrMfus7QywvfVqYibAj|1|1716093802|1716101864|NORMAL|今日やりたいこと:
- [x] ChatGPT subscription stopできるか
- [x] その額でサーバーを用意できるか?
memoテーブルの列名を確認
PRAGMA table_info(memo);
↓
いい感じに表示される
0|id|INTEGER|0||1
1|uid|TEXT|1||0
2|creator_id|INTEGER|1||0
3|created_ts|BIGINT|1|strftime('%s', 'now')|0
4|updated_ts|BIGINT|1|strftime('%s', 'now')|0
5|row_status|TEXT|1|'NORMAL'|0
6|content|TEXT|1|''|0
7|visibility|TEXT|1|'PRIVATE'|0
8|tags|TEXT|1|'[]'|0
9|payload|TEXT|1|'{}'|0
SQLiteを終了する
.exit
コンテナも終了する
exit
memosのdbとblueskyのデータをいい感じに適用させる。
memosの列 | BlueSkyのキー | 備考 |
---|---|---|
uid | cid | ユニークな識別子 |
creator_id | author.did | 投稿の作成者のID →memosへ同期時はbot idに書き換える |
created_ts, updated_ts | created_at | 投稿の作成時刻と更新時刻。UNIXタイムスタンプで保存し、BlueSkyの日付をUNIXタイムスタンプに変換して使用。 → created_tsをcreated_atにすることにした。 |
row_status | N/A | 行のステータス。デフォルトは 'NORMAL' と設定されている |
content | record.text | 投稿の内容 |
visibility | N/A | 投稿の可視性 |
tags | N/A | 投稿に関連付けられたタグ |
payload | N/A | その他のペイロード。必要に応じてJSON形式で保存 |
uit=cid
creator_id=author.did
created_ts=created_at
row_status=NULL
visiblity=PUBLIC
tags=NULL
payload="property":{}
左辺がmemos、右辺がbluesky
sql = '''
INSERT INTO memo (uid, creator_id, created_ts, updated_ts, row_status, content, visibility, tags, payload)
VALUES (?, ?, ?, ?, 'NORMAL', ?, 'PUBLIC', '[]', '{"property":{}}')
'''
apiというより、bot用アカウントを動かすプログラムを作った。
以下はラフにメモした記録。
作業中やたらとお世話になったコマンド:
docker ps
docker stop memos
コンテナイメージをcommitしてイメージを作ったら意味わからなくなったときの脱出ログ:
本家のイメージと自分がcommitしたイメージ(mymemosimage)とが両方動いてるから、一旦本家のイメージだけにすべく、mymemosimageを削除してみたら反映された
docker stop memos
docker rm memos
npm run start
※ startにはあらかじめ以下のコマンドを登録している
docker start memos || docker run -d --init --name memos --publish 5230:5230 --volume ~/memos/data:/var/opt/memos neosmemo/memos:stable && echo 'Memos is running at http://localhost:5230'
バックアップはcpでコピーが無難
cp ~/memos/data/memos_prod.db ~/memos/data/memos_prod_backup.db
コンフリクトを整えてから進める:
コンテナ内データをローカルにコピーする
docker cp memos:/var/opt/memos/memos_prod.db ~/memos/data/memos_prod.db
場合によってはローカルをコンテナ内へコピーしたいときもある
docker cp ~/memos/data/memos_prod.db memos:/var/opt/memos/memos_prod.db
ローカルdbの内容確認
sqlite3 ~/memos/data/memos_prod.db
.tables
.schema memo
SELECT * FROM memo;
ローカルのsqliteの脱出はCtrl+Cを2連続
不具合であろうデータを取り除く:
指定した行(2,3)を削除
DELETE FROM memo WHERE id IN (2, 3);
SELECT * FROM memo;
リスタートしたら反映されてた!!
docker restart memos
blueskyからの取り込み時のデータを既存のmemosのデータと合わせられるように整形する
DELETE FROM memo WHERE id IN (7);
桁数微妙に合わせるのに苦労した。
python実行→確認→削除→python実行→確認→….繰り返し
sqlite> SELECT * FROM memo;
1|aRE7cHkNRUVcR6fdf7mF4R|1|1716563427|1716563427|NORMAL|あいうえお|PRIVATE|[]|{"property":{}}
4|42a6GbtKCWD2PMuJgm3m9t|2|1716604506|1716604506|NORMAL|blueアカウントからこんにちは|PUBLIC|[]|{"property":{}}
5|KbxNBbxGVKhJ2ShvgQZ3sm|1|1716604565|1716604565|NORMAL|heroroアカからこんにちは|PROTECTED|[]|{"property":{}}
6|nnkn54m5u7kwjffogut64gyzabtopdni|2|1716408223|1716408223|NORMAL|とりあえずここを
『とりあえずbox』にして動かしてみよう|PUBLIC|[]|{"property":{}}
sqlite> DELETE FROM memo WHERE id IN (6);
sqlite> SELECT * FROM memo;
1|aRE7cHkNRUVcR6fdf7mF4R|1|1716563427|1716563427|NORMAL|あいうえお|PRIVATE|[]|{"property":{}}
4|42a6GbtKCWD2PMuJgm3m9t|2|1716604506|1716604506|NORMAL|blueアカウントからこんにちは|PUBLIC|[]|{"property":{}}
5|KbxNBbxGVKhJ2ShvgQZ3sm|1|1716604565|1716604565|NORMAL|heroroアカからこんにちは|PROTECTED|[]|{"property":{}}
sqlite> SELECT * FROM memo;
1|aRE7cHkNRUVcR6fdf7mF4R|1|1716563427|1716563427|NORMAL|あいうえお|PRIVATE|[]|{"property":{}}
4|42a6GbtKCWD2PMuJgm3m9t|2|1716604506|1716604506|NORMAL|blueアカウントからこんにちは|PUBLIC|[]|{"property":{}}
5|KbxNBbxGVKhJ2ShvgQZ3sm|1|1716604565|1716604565|NORMAL|heroroアカからこんにちは|PROTECTED|[]|{"property":{}}
7|wjffogut64gyzabtopdni|2|1716408223|1716408223|NORMAL|とりあえずここを
『とりあえずbox』にして動かしてみよう|PUBLIC|[]|{"property":{}}
sqlite> DELETE FROM memo WHERE id IN (7);
sqlite> SELECT * FROM memo;
1|aRE7cHkNRUVcR6fdf7mF4R|1|1716563427|1716563427|NORMAL|あいうえお|PRIVATE|[]|{"property":{}}
4|42a6GbtKCWD2PMuJgm3m9t|2|1716604506|1716604506|NORMAL|blueアカウントからこんにちは|PUBLIC|[]|{"property":{}}
5|KbxNBbxGVKhJ2ShvgQZ3sm|1|1716604565|1716604565|NORMAL|heroroアカからこんにちは|PROTECTED|[]|{"property":{}}
sqlite> SELECT * FROM memo;
1|aRE7cHkNRUVcR6fdf7mF4R|1|1716563427|1716563427|NORMAL|あいうえお|PRIVATE|[]|{"property":{}}
4|42a6GbtKCWD2PMuJgm3m9t|2|1716604506|1716604506|NORMAL|blueアカウントからこんにちは|PUBLIC|[]|{"property":{}}
5|KbxNBbxGVKhJ2ShvgQZ3sm|1|1716604565|1716604565|NORMAL|heroroアカからこんにちは|PROTECTED|[]|{"property":{}}
8|kwjffogut64gyzabtopdni|2|1716408223|1716408223|NORMAL|とりあえずここを
『とりあえずbox』にして動かしてみよう|PUBLIC|[]|{"property":{}}
コンテナ内の様子も確認
docker exec -it memos /bin/sh
/usr/local/memos # cd /var/opt/memos
/var/opt/memos # sqlite3 memos_prod.db
SQLite version 3.44.2 2023-11-24 11:41:44
Enter ".help" for usage hints.
sqlite> SELECT * FROM memo;
1|aRE7cHkNRUVcR6fdf7mF4R|1|1716563427|1716563427|NORMAL|あいうえお|PRIVATE|[]|{"property":{}}
4|42a6GbtKCWD2PMuJgm3m9t|2|1716604506|1716604506|NORMAL|blueアカウントからこんにちは|PUBLIC|[]|{"property":{}}
5|KbxNBbxGVKhJ2ShvgQZ3sm|1|1716604565|1716604565|NORMAL|heroroアカからこんにちは|PROTECTED|[]|{"property":{}}
8|kwjffogut64gyzabtopdni|2|1716408223|1716408223|NORMAL|とりあえずここを
『とりあえずbox』にして動かしてみよう|PUBLIC|[]|{"property":{}}
入った🙌
def fetch_author_posts(username, password, author_did):
client = Client(base_url='https://bsky.social')
client.login(username, password)
response = client.get_author_feed(
actor=author_did,
filter='posts_and_author_threads',
limit=5
)
data = response.json() # レスポンスをJSON形式に変換
# 応答が文字列としてエンコードされている場合、JSONとして再解析
if isinstance(data, str):
data = json.loads(data)
return data
def insert_data_into_memos(db_path, post_data):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 既存のUIDを取得
cursor.execute("SELECT uid FROM memo")
existing_uids = {row[0] for row in cursor.fetchall()}
# 新しいデータをデータベースに挿入
for post in post_data['feed']:
uid_original = post['post']['cid']
uid = uid_original[-22:] # CIDから最後の22文字を取得 (21文字が必要な場合は-22を使用)
if uid not in existing_uids:
creator_id = 2
created_ts = int(parser.parse(post['post']['record']['created_at']).timestamp()) # dateutil.parserを使用して日付を解析
content = post['post']['record']['text']
sql = '''
INSERT INTO memo (uid, creator_id, created_ts, updated_ts, row_status, content, visibility, tags, payload)
VALUES (?, ?, ?, ?, 'NORMAL', ?, 'PUBLIC', '[]', '{"property":{}}')
'''
cursor.execute(sql, (uid, creator_id, created_ts, created_ts, content))
print(f"Inserted post with uid: {uid}")
else:
print(f"Skipped post with existing uid: {uid}")
...🐌...
restartしてから実行するのが一番簡単だから、scriptに登録することにした。
"scripts": {
"bluesky": "docker restart memos && python bluesky_authorTLposts.py && docker restart memos"
}
できた流れは…
ローカルでmemosに投稿する
ちょっとblueskyを眺めたくなる
blueskyで投稿する
ローカルの生活に戻る。
そういえばblueskyで投稿したこと見返したいな〜ってなる。
npm run bluesky
memosのexploreを開くと反映される
そこから再びひらめいて作業をする。。。。
ローカルバンザイ 🙌
本当はThreadsとかInstagramをつなげたいけど、
blueskyの通信が結構簡単だったから今回はやってみた。
また気が向いたら拡張させた〜〜〜い❤️