Skip to content

Instantly share code, notes, and snippets.

@eallion
Last active March 8, 2024 20:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eallion/bf8861eb3292c2351c1067fba3198c26 to your computer and use it in GitHub Desktop.
Save eallion/bf8861eb3292c2351c1067fba3198c26 to your computer and use it in GitHub Desktop.
Mastodon 同步到 Memos 脚本

Mastodon 同步到 Memos 脚本

利用 Mastodon 的 Webhook 可以主动同步 Status 到 Memos(或其他平台),而不是利用 RSS 或 Crontab 被动式同步。

已测试版本

安装工具:

请安装工具,若有报错,请安装其他对应工具

  • sudo apt install jq
  • sudo apt install lynx

配置

以下为 Shell Script 脚本内容,请注意替换:

  • 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

查找 ID: https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME

JSON 数据文件内容

.mastodon_memos_id.json

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

脚本内容

mastodon_sync_to_memos.sh

#!/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

# 获取当前用户的家目录路径及保存 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 "============================="
{
"latest_memos_id": "",
"latest_mastodon_id": "",
"bind": []
}
#!/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
# 获取当前用户的家目录路径及保存 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 "============================="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment