eallion

大大的小蜗牛

机会总是垂青于有准备的人!
mastodon
github
twitter
steam
telegram
keybase
email

Mastodon を Memos に同期

最新のスクリプト:https://gist.github.com/eallion/bf8861eb3292c2351c1067fba3198c26

更新:宝塔パネルの例を追加しました。

TL;DR#

このページに直接ジャンプして スクリプト内容 を確認してください。

前言#

運が良いのか悪いのか分かりませんが、Memos を主力ツールとして私のワークフローに加えようとしているときに、v0.19.0 のバージョン更新に遭遇しました。このバージョン更新は一連の大きな問題をもたらしました。Memos の新バージョンの堅牢性が疑問視されているだけでなく、それを搭載したサーバー自体も影響を受け、64G メモリの物理マシンでも動作しませんでした。ネットユーザーが言うように、Memos はただの練習用プロジェクトのようです。思い切って捨てました。Google Keep や Obsidian などのツールはどこが悪いのでしょうか?すべてのタスクを一つのツールに束縛しないのは確かに少し面倒ですが、All in one は基本的に All in boom でもあります。
今、私は Memos を私の Mastodon のバックアップ用として位置付けています(その一つの方法です)。

私は Webhook のような能動的なプッシュ方式が好きです。RSS や Cron のような受動的なプル方式よりもシンプルで、低炭素で環境に優しく、即時性があります。主に能動的な感覚を掌握することがとても爽快です。

ここからは、Mastodon を利用して Webhook で Memos にツイートを同期する方法を紹介します。私は Shell Script を使用しており、非常にシンプルなスクリプトで、いくつかの常識的な論理判断を行っていますが、完璧ではないかもしれません。Node.js や Python などでも実現可能です。

テスト済みバージョン#

Mastodon には自分のインスタンスが必要で、または Webhook を作成できる管理者権限を持つアカウントが必要です。

ツールのインストール#

サーバーにツールをインストールしてください。エラーが発生した場合は、エラーログに基づいて他の対応するツールをインストールしてください。

  • sudo apt install jq
  • sudo apt install lynx

Webhook ツール#

Mastodon に移動して https://{INSTANCE}/admin/webhooks で Webhook を作成します。
イベントは status:created のみを選択できます。返信やリブログもこのイベントに含まれます。
目的地 URL には自分がデプロイした Webhook のリンクを入力します。例:https://webhook.example.com/hooks/mastodon-sync-to-memos
宝塔のものは:https://webhook.mybtserver.com:8888/hook?access_key=ACCESSKEY
Mastodon の Webhook 目的地 URL はドメインにバインドすることをお勧めします。さもなければ、Sidekiq が処理できない可能性があります。

スクリプト内容#

以下のスクリプト内容をサーバー上の .sh ファイルに保存します。例えば、現在のユーザーのホームディレクトリ(~)の ~/mastodon_sync_to_memos.sh ファイルに保存し、以下の内容を設定してください。置き換えに注意してください:

  • MEMOS_HOST=""
  • MEMOS_ACCESS_TOKEN=""
  • MEMOS_VISIBILITY=""
  • MASTODON_INSTANCE=""
  • MASTODON_ID=""
  • SKIP_MASTODON_REPLY=
  • SKIP_MASTODON_REBLOG=
  • HOME_DIR=~
  • FILE_PATH=$HOME_DIR/.mastodon_memos_id.json

Mastodon ID を探す: https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME

#!/bin/bash

# テスト済みバージョン: 
# Memos: v0.18.2 
# Mastodon: v4.2.8

# ======================================================
# 設定開始

# Memos ホスト
MEMOS_HOST=""

# Memos アクセストークン
MEMOS_ACCESS_TOKEN=""

# Memos の可視性 ('PUBLIC', 'PROTECTED', 'PRIVATE') のいずれかを選択
MEMOS_VISIBILITY=PUBLIC

# Mastodon インスタンス
MASTODON_INSTANCE=""

# Mastodon ID, ID を見つける: https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME
MASTODON_ID=""

# 返信とリブログをスキップ
SKIP_MASTODON_REPLY=true
SKIP_MASTODON_REBLOG=true

# 現在のユーザーのホームディレクトリのパスと ID を保存するファイルを取得し、デフォルトを保持し、変更しないでください
HOME_DIR=~
FILE_PATH=$HOME_DIR/.mastodon_memos_id.json

# 設定終了
# ======================================================

# 以下の内容は変更しないでください

# ID ファイルが存在するか確認
if [ ! -f "$FILE_PATH" ]; then
  # ファイルが存在しない場合、ファイルを作成し JSON データを書き込みます
  echo '
{
  "latest_memos_id": "0",
  "latest_mastodon_id": "0",
  "bind": []
}
' > "$FILE_PATH"
  echo "データファイルが作成されました: $FILE_PATH"
else
  # ファイルが存在する場合、スキップして次のステップに進みます
  echo "ローカルデータが存在するため、スキップします..."
fi

# API とトークンを結合
if [[ "$MEMOS_HOST" != */ ]]; then
  MEMOS_HOST="$MEMOS_HOST/"
fi
MEMOS_API_HOST="${MEMOS_HOST}api/v1/memo"
AUTHORIZATION="Bearer ${MEMOS_ACCESS_TOKEN}"

# Memos の最新の Memos ID を取得
MEMOS_URL="${MEMOS_API_HOST}?creatorId=101&rowStatus=NORMAL&limit=1"
LATEST_MEMOS_ID=$(curl --connect-timeout 60 -s $MEMOS_URL | jq -r '.[0].id')

# Mastodon の API
if [[ "$MASTODON_INSTANCE" != */ ]]; then
  MASTODON_INSTANCE="$MASTODON_INSTANCE/"
fi
CONTENT_URL="${MASTODON_INSTANCE}api/v1/accounts/${MASTODON_ID}/statuses?limit=1&exclude_replies=${SKIP_MASTODON_REPLY}&exclude_reblogs=${SKIP_MASTODON_REBLOG}"

# Mastodon の最新ステータスの ID
LATEST_MASTODON_ID=$(curl --connect-timeout 60 -s $CONTENT_URL | jq -r '.[0].id')

# LOCAL_MEMOS_ID 変数を定義
LOCAL_MEMOS_ID=$(cat "$FILE_PATH" | jq -r '.latest_memos_id')
LOCAL_MASTODON_ID=$(cat "$FILE_PATH" | jq -r '.latest_mastodon_id')

# Webhook がトリガーされたとき、Mastodon の最新 ID がローカル ID と同じかどうかを判断し、重複同期を防ぎます
if [ "$LATEST_MASTODON_ID" == "$LOCAL_MASTODON_ID" ]; then
  echo "Mastodon は更新されていません、スキップします..."
  echo "スキップされた: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
  echo "============================="
  exit 0
fi

CONTENT=$(curl --connect-timeout 60 -s $CONTENT_URL | jq -r '.[0]')

MEDIA=$(echo $CONTENT | jq -r '.media_attachments')
# Media の内容を判断
if [ "$MEDIA" != "null" ]; then
  MEDIAS=$(echo $CONTENT | jq -r '.media_attachments[] | select(.type=="image") | .url')
  # 画像を結合
  images=""
  for url in $MEDIAS; do 
    images="$images![image]($url)\n"
  done
  TEXT=$(echo "$CONTENT" | jq -r '.content' | lynx -dump -stdin -nonumbers -nolist | tr -d '\n' | sed '/^$/N;s/\n\n/\n/g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed -E 's/ {2,}/ /g')
  TEXT="$TEXT\n$images"
else
  # 通常の内容
  TEXT=$(echo "$CONTENT" | jq -r '.content' | lynx -dump -stdin -nonumbers -nolist | tr -d '\n' | sed '/^$/N;s/\n\n/\n/g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed -E 's/ {2,}/ /g')
fi

# 内容が空かどうかを判断
if [ -z "$TEXT" ] || [ "$TEXT" == "\\n" ]; then
  echo "内容が空です、スキップします..."
  echo "スキップされた: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
  echo "============================="
  exit 0
fi

# ダブルクオートをエスケープ
TEXT=$(echo "$TEXT" | sed 's/"/\\"/g')

# Webhook がトリガーされたとき、Memos の最新 ID がローカル ID と同じかどうかを判断
# Memos が一方的に更新された後、Mastodon と Memos の ID のバインディング関係を検証します(Todo)
#if [ "$LATEST_MEMOS_ID" == "$LOCAL_MEMOS_ID" ]; then
#  echo "Memos は更新されていません、スキップします..."
#  echo "スキップされた: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
#  echo "============================="
# exit 0
#fi

# Mastodon と Memos の Content 内容の MD5 値を比較します(必ずしも正確ではありません)
# 後で GPT を導入して内容を比較することを試みます
CONTENT_MEMOS=$(curl --connect-timeout 60 -s $MEMOS_URL | jq '.[0].content')
CONTENT_MASTODON=$TEXT

# 最新の Memos の MD5 を取得
LATEST_MEMOS_MD5=$(echo $CONTENT_MEMOS | tr -d '"' | md5sum | cut -d' ' -f1)
# 最新の Mastodon の MD5 を取得
LATEST_TEXT_MD5=$(echo $TEXT | tr -d '"' | md5sum | cut -d' ' -f1)

# MD5 を通じて内容が重複しているかどうかを判断
if [ "$LATEST_TEXT_MD5" == "$LATEST_MEMOS_MD5" ]; then
  echo "内容が重複しています、スキップします..."
  echo "スキップされた: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
  echo "============================="
  exit 0
fi

# NeoDB の評価 Emoji を置き換え
TEXT=$(echo "$TEXT" | sed "s/:star_empty:/🌑/g; s/:star_half:/🌗/g; s/:star_solid:/🌕/g")

# 最後の空行を削除
TEXT=$(echo "$TEXT" | sed 's/\\n$//')

# Memos に公開し、返された JSON データを取得
RESPONSE=$(curl -s -X POST \
  -H "Accept: application/json" \
  -H "Authorization: $AUTHORIZATION" \
  -d "{ \"content\": \"$TEXT\", \"visibility\": \"$MEMOS_VISIBILITY\"}" \
  $MEMOS_API_HOST)

# 返された JSON データから Memos の id 値を抽出
NEW_MEMOS_ID=$(echo "$RESPONSE" | jq -r '.id')

# JSON ファイル内の latest_memos_id の値を更新
jq ".latest_memos_id = \"$NEW_MEMOS_ID\"" "$FILE_PATH" > "${FILE_PATH}.tmp" && mv "${FILE_PATH}.tmp" "$FILE_PATH"

# JSON ファイル内の latest_mastodon_id の値を更新
jq ".latest_mastodon_id = \"$LATEST_MASTODON_ID\"" "$FILE_PATH" > "${FILE_PATH}.tmp" && mv "${FILE_PATH}.tmp" "$FILE_PATH"

# Mastodon と Memos の ID のバインディング関係を更新し、"bind" 内の配列が一意のキーを保持し、キーも一意の値のみを持つことを確認します
jq ".bind += [{\"$LATEST_MASTODON_ID\": \"$NEW_MEMOS_ID\"}] | .bind = (.bind | unique)" "$FILE_PATH" > "${FILE_PATH}.tmp" && mv "${FILE_PATH}.tmp" "$FILE_PATH"

echo "Mastodon を Memos に同期しました!"
echo "完了: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
echo "============================="

宝塔パネル#

宝塔パネルで Webhook プラグインを使用する場合、上記のスクリプト内容を Webhook プラグインのスクリプトに直接コピーできます。サーバーで手動で .sh ファイルを作成する必要はありません。

JSON データファイルの内容#

スクリプトを初めて実行すると、現在のユーザーのホームディレクトリ ~ に新しいファイル ~/.mastodon_memos_id.json が作成され、初期化されます。以降、このファイルは Mastodon ID と Memos ID のバインディング関係を記録します。ホームディレクトリに作成したくない場合は、HOME_DIR=FILE_PATH= の 2 つのパラメータを変更する必要があります。

{
  "latest_memos_id": "",
  "latest_mastodon_id": "",
  "bind": []
}

本番環境で生成されたデータの例:

{
  "latest_memos_id": "6231",
  "latest_mastodon_id": "112061852482921394",
  "bind": [
    {
      "112059053750743781": "6230"
    },
    {
      "112061852482921394": "6231"
    }
  ]
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。