eallion

大大的小蜗牛

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

Mastodon 同步到 Memos

最新腳本:https://gist.github.com/eallion/bf8861eb3292c2351c1067fba3198c26

Update:添加寶塔面板的示例。

TL;DR#

直接跳轉到本頁 腳本內容 查看腳本代碼。

前言#

不知道是我運氣好還是不好,在我準備把 Memos 當成主力工具加入到我的工作流中的時候,遇上了 v0.19.0 的版本更新,這次版本更新帶來了一系列大坑。除了 Memos 新版本的魯棒性備受質疑,甚至連搭載它的伺服器本身也被影響,我在 64G 內存的物理機上都跑不動它。就像網友所說,Memos 只像是一個練手的項目。果斷棄之。Google Keep、Obsidian 這些工具哪裡不好了嗎?不把所有事務約束在一個工具裡確實是麻煩了一點,但是 All in one 基本上也等於是 All in boom。
現在我對 Memos 的定位是用來備份我的 Mastodon(方式之一)。

我一直喜歡 Webhook 這種主動式 Push 的方案,比 RSS、Cron 計畫任務這些被動式 Pull 的方案簡潔低碳環保,更具有即時性。主要是掌握主動的感覺讓人覺得很爽。

下面開始介紹一下 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 文件中,如當前用戶的 Home 目錄(~)的 ~/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 Host
MEMOS_HOST=""

# Memos Access Token
MEMOS_ACCESS_TOKEN=""

# 發布 Memos 的可見性 ('PUBLIC', 'PROTECTED', 'PRIVATE') 三選一
MEMOS_VISIBILITY=PUBLIC

# Mastodon Instance
MASTODON_INSTANCE=""

# Mastodon ID, Find ID: https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME
MASTODON_ID=""

# 跳過回覆和轉嘟
SKIP_MASTODON_REPLY=true
SKIP_MASTODON_REBLOG=true

# 獲取當前用戶的 Home 目錄路徑及保存 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 "Data file created: $FILE_PATH"
else
  # 如果文件存在,則跳過並進行後續步驟
  echo "Local data exist, skipping..."
fi

# 拼接 API 和 Token
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 最新 Status 的 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 no updated, skipping..."
  echo "Skipped: $(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 "Content is empty, skipping..."
  echo "Skipped: $(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 no updated, skipping..."
#  echo "Skipped: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
#  echo "============================="
# exit 0
#fi

# 對比 Matodon 和 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 "Content is duplicate, skipping..."
  echo "Skipped: $(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 "Sync Mastodon to Memos Successful!"
echo "Done: $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
echo "============================="

寶塔面板#

寶塔面板如果用 Webhook 插件,可以直接把上面的腳本內容複製到 Webhook 插件的腳本中。不用另外在伺服器中手動創建 .sh 文件。

JSON 數據文件內容#

初次運行腳本,它會在當前用戶的 Home 目錄 ~ 新建一個文件 ~/.mastodon_memos_id.json 並初始化,後續此文件會記錄 Mastodon ID 和 Memos ID 的綁定關係。如果不想在 Home 目錄創建,就需要修改 HOME_DIR=FILE_PATH= 這兩個參數。

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

生產環境產生數據後,示例:

{
  "latest_memos_id": "6231",
  "latest_mastodon_id": "112061852482921394",
  "bind": [
    {
      "112059053750743781": "6230"
    },
    {
      "112061852482921394": "6231"
    }
  ]
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。