自建 Matrix Synapse 做媒体维护之前,最好先告诉用户规则。否则用户只知道“为什么不建议发大文件”,不知道服务器正在控制媒体仓库体积。
这里说的 media_store 不是“视频文件夹”。它是 Synapse 的媒体仓库,聊天里上传或缓存下来的媒体类内容都会放到这里。常见内容包括:
- 聊天图片、截图、表情包、动图和视频。
- 语音消息、音频文件、PDF、压缩包、文档等普通附件。
- 用户头像和房间头像。
- Synapse 为图片、视频等生成的缩略图。
- 远程服务器媒体缓存,也就是本服用户查看过的其他服务器上的图片或文件副本。
- URL 预览缓存,例如链接预览抓取到的图片和缩略图。
所以媒体维护规则影响的不是某一种文件,而是整个 Synapse 媒体仓库。本地上传的大图片、动图、视频、语音、文档和压缩包都会按大小和时间进入清理规则;远程服务器媒体缓存会定期清理;头像和房间头像也建议控制在 5MiB 以内,避免变成长期的大媒体负担。
本文目标是用 Synapse 官方的 Server Notices 给本服用户发送一条系统通知,内容类似:
5MiB 以内文件长期保留
超过 5MiB 的文件,90 天后清理
超过 100MiB 的超大文件 7 天后清理
重要文件请自行保存这里不是做“每次上传都实时提醒”。实时拦截上传需要额外模块或反代日志分析,复杂度更高。Server Notices 更适合做上线公告、规则变更通知和新用户补发。
关于头像这条规则要先说清楚:清理脚本使用 keep_profiles=true,会尽量保留仍被用户头像、房间头像等 profile image 使用的媒体。通知里写“头像和房间头像建议不要超过 5MiB”,主要是为了减少客户端加载压力和长期存储负担。
官方参考:
- Server Notices 配置说明:https://element-hq.github.io/synapse/latest/server_notices.html
- Server Notices Admin API:https://element-hq.github.io/synapse/latest/admin_api/server_notices.html
一、准备信息
下面示例统一使用这些占位值:
Matrix server_name: 666666.xyz
Synapse 本机地址: http://127.0.0.1:8008
管理员 token 文件: /root/.config/synapse-admin/access_token
Server Notices 系统账号: @server:666666.xyz
测试接收用户: @alice:666666.xyz实际操作时必须改成自己的值:
666666.xyz 改成你的 Matrix server_name
@server:666666.xyz 改成你的 Server Notices 系统账号
@alice:666666.xyz 改成要测试接收通知的真实用户
/root/.config/synapse-admin/access_token 改成你实际保存管理员 token 的文件路径文中的:
TOKEN_FILE="/root/.config/synapse-admin/access_token"
TOKEN="$(cat "$TOKEN_FILE")"不是固定 token。它的意思是从 TOKEN_FILE 读取你自己的 Synapse 管理员 access token。如果你的 token 存在别的位置,要同步改掉 TOKEN_FILE。
先确认 token 有效:
TOKEN_FILE="/root/.config/synapse-admin/access_token"
TOKEN="$(cat "$TOKEN_FILE")"
curl -sS \
-H "Authorization: Bearer ${TOKEN}" \
http://127.0.0.1:8008/_matrix/client/v3/account/whoami
echo正常会返回类似:
{"user_id":"@media_purge_admin:666666.xyz","is_guest":false,"device_id":"MEDIA_PURGE_CRON"}再确认它能访问 Admin API:
TOKEN_FILE="/root/.config/synapse-admin/access_token"
TOKEN="$(cat "$TOKEN_FILE")"
curl -sS -o /tmp/synapse-admin-check.json -w 'http_code=%{http_code}\n' \
-H "Authorization: Bearer ${TOKEN}" \
'http://127.0.0.1:8008/_synapse/admin/v2/users?from=0&limit=1&guests=false'
cat /tmp/synapse-admin-check.json
echo
rm -f /tmp/synapse-admin-check.json看到 http_code=200 才继续。401 通常是 token 失效,403 通常是这个用户不是管理员。
二、检查系统通知账号是否被占用
Server Notices 需要一个本地系统账号。本文使用:
@server:666666.xyz先查这个用户是否已经存在:
TOKEN_FILE="/root/.config/synapse-admin/access_token"
TOKEN="$(cat "$TOKEN_FILE")"
curl -sS -o /tmp/server-user-check.json -w 'http_code=%{http_code}\n' \
-H "Authorization: Bearer ${TOKEN}" \
'http://127.0.0.1:8008/_synapse/admin/v2/users/@server:666666.xyz'
cat /tmp/server-user-check.json
echo
rm -f /tmp/server-user-check.json如果返回类似:
http_code=404并且 JSON 里有:
{"errcode":"M_NOT_FOUND","error":"User not found"}说明 @server:666666.xyz 没有被普通用户占用,可以作为 Server Notices 系统账号。
如果返回 200,说明这个用户已经存在。不要直接拿已存在的普通用户当系统通知账号,建议换一个 localpart,例如:
@notice:666666.xyz
@server_notice:666666.xyz后文配置里的 system_mxid_localpart 也要跟着改。
三、启用 Server Notices
编辑 Synapse 配置:
vi /root/data/docker_data/synapse/data/homeserver.yaml在文件末尾加入:
server_notices:
system_mxid_localpart: server
system_mxid_display_name: "服务器通知"
room_name: "服务器通知"
auto_join: true这里的 system_mxid_localpart: server 会生成:
@server:<你的 server_name>如果你想使用 @notice:666666.xyz,就写:
server_notices:
system_mxid_localpart: notice
system_mxid_display_name: "服务器通知"
room_name: "服务器通知"
auto_join: true重启 Synapse:
cd /root/data/docker_data/synapse
docker compose restart synapse等接口恢复:
curl -i http://127.0.0.1:8008/_matrix/client/versions如果返回 HTTP/1.1 200 OK,说明 Synapse 已恢复。
四、先给单个用户测试
不要一上来群发。先挑一个自己的测试用户,例如:
@alice:666666.xyz生成通知 JSON:
python3 - <<'PY' > /tmp/media-policy-notice.json
import json
body = """【服务器媒体保留规则】
为了避免 Matrix 媒体仓库长期堆积大文件,本服务器已启用媒体分层清理:
- 5MiB 以内文件长期保留。
- 超过 5MiB 的文件,90 天后清理。
- 超过 100MiB 的超大文件 7 天后清理。
请自行保存重要大文件。"""
print(json.dumps({
"user_id": "@alice:666666.xyz",
"content": {
"msgtype": "m.notice",
"body": body,
},
}, ensure_ascii=False))
PY把 @alice:666666.xyz 改成真实测试用户,然后发送:
TOKEN_FILE="/root/.config/synapse-admin/access_token"
TOKEN="$(cat "$TOKEN_FILE")"
curl -sS -X PUT \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data @/tmp/media-policy-notice.json \
'http://127.0.0.1:8008/_synapse/admin/v1/send_server_notice/media-policy-v4-tiered-alice'
echo
rm -f /tmp/media-policy-notice.json正常会返回类似:
{"event_id":"$example_event_id"}然后登录测试用户,确认能看到一个名为“服务器通知”的房间和刚才那条通知。
这里的 URL 最后有一个 media-policy-v4-tiered-alice,它是 transaction id。相同 transaction id 重复请求时,Synapse 会按幂等逻辑处理,避免重复发送同一条通知。
五、批量发送给本服用户
单用户测试正常后,可以写成脚本批量发送。
创建脚本:
mkdir -p /root/data/docker_data/synapse/scripts
vi /root/data/docker_data/synapse/scripts/send-media-policy-notice.py脚本内容如下。注意先改 SERVER_NAME、TOKEN_PATH 和 EXCLUDE:
#!/usr/bin/env python3
import json
import urllib.error
import urllib.parse
import urllib.request
BASE = "http://127.0.0.1:8008"
TOKEN_PATH = "/root/.config/synapse-admin/access_token"
SERVER_NAME = "666666.xyz"
# 不给媒体清理专用账号和 Server Notices 系统账号发通知。
EXCLUDE = {
f"@media_purge_admin:{SERVER_NAME}",
f"@server:{SERVER_NAME}",
}
# 规则版本号。通知内容更新时,改这里,例如 media-policy-v5-20260601-。
TXN_PREFIX = "media-policy-v4-tiered-20260523-"
BODY = """【服务器媒体保留规则】
为了避免 Matrix 媒体仓库长期堆积大文件,本服务器已启用媒体分层清理:
- 5MiB 以内文件长期保留。
- 超过 5MiB 的文件,90 天后清理。
- 超过 100MiB 的超大文件 7 天后清理。
请自行保存重要大文件。"""
CONTENT = {
"msgtype": "m.notice",
"body": BODY,
}
with open(TOKEN_PATH, "r", encoding="utf-8") as f:
TOKEN = f.read().strip()
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
}
JSON_HEADERS = {
**HEADERS,
"Content-Type": "application/json",
}
def request_json(method, url, payload=None):
data = None if payload is None else json.dumps(payload).encode("utf-8")
headers = JSON_HEADERS if payload is not None else HEADERS
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
raw = resp.read().decode("utf-8")
return resp.status, json.loads(raw) if raw else {}
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8")
try:
parsed = json.loads(raw)
except Exception:
parsed = {"raw": raw}
return exc.code, parsed
users = []
start = 0
while True:
url = f"{BASE}/_synapse/admin/v2/users?from={start}&limit=100&guests=false"
status, data = request_json("GET", url)
if status != 200:
raise SystemExit(f"list users failed: HTTP {status} {data}")
for user in data.get("users", []):
mxid = user.get("name")
if not mxid or not mxid.endswith(f":{SERVER_NAME}"):
continue
if mxid in EXCLUDE:
continue
if user.get("deactivated") or user.get("is_guest"):
continue
users.append(mxid)
next_token = data.get("next_token")
if next_token is None:
break
start = int(next_token)
sent = []
failed = []
for mxid in users:
encoded_user_id = urllib.parse.quote(mxid, safe="")
txn = TXN_PREFIX + encoded_user_id
url = f"{BASE}/_synapse/admin/v1/send_server_notice/{txn}"
payload = {
"user_id": mxid,
"content": CONTENT,
}
status, data = request_json("PUT", url, payload)
if 200 <= status < 300:
sent.append({"user_id": mxid, "event_id": data.get("event_id")})
else:
failed.append({"user_id": mxid, "http_code": status, "response": data})
print(json.dumps({
"target_count": len(users),
"sent": sent,
"failed": failed,
}, ensure_ascii=False, indent=2))
if failed:
raise SystemExit(1)授权:
chmod 700 /root/data/docker_data/synapse/scripts/send-media-policy-notice.py运行:
/root/data/docker_data/synapse/scripts/send-media-policy-notice.py成功输出类似:
{
"target_count": 5,
"sent": [
{
"user_id": "@admin:666666.xyz",
"event_id": "$example1"
},
{
"user_id": "@alice:666666.xyz",
"event_id": "$example2"
}
],
"failed": []
}六、以后新增用户怎么办
如果 TXN_PREFIX 不变,脚本重复运行时,已经收到过这一版通知的老用户不会重复收到同一条。
以后新增用户后,可以手动再跑一次:
/root/data/docker_data/synapse/scripts/send-media-policy-notice.py新用户会收到通知,老用户通常不会重复收到。
如果只是给后来新增的用户补发同一版媒体规则通知,TXN_PREFIX 不用改。
如果是新内容、新公告、新规则,就要同时改通知内容和 TXN_PREFIX。例如规则内容从“按大小到期清理”改成“安全维护公告”,就改成:
TXN_PREFIX = "media-policy-v4-20260601-"这样所有用户都会收到新版通知。否则你只改 BODY,但 TXN_PREFIX 不变,Synapse 可能会认为这是同一个 transaction,同一用户不会重复收到。
简单说:
同一版通知补发给新用户:不改 TXN_PREFIX
新通知、新规则、新公告:修改 BODY,也修改 TXN_PREFIX七、已经发出去的通知能不能修改
不能原地修改。
Server Notices 发出去后,本质上就是 Matrix 房间里的一个事件。事件内容已经进入用户的通知房间,不能像编辑网页公告那样统一改掉。
如果通知内容写错了,常见处理方式有三种:
1. 直接发一条新版通知,写明“以本通知为准”
2. 撤回旧通知,再发新版通知
3. 旧通知保留,新通知解释规则已经更新实际运维里,我更推荐第 1 种或第 3 种。因为撤回后用户看到的通常是“消息已删除/已撤回”,反而可能让人困惑。
推荐做法:发新版通知
把 BODY 改成新版内容,同时修改 TXN_PREFIX:
TXN_PREFIX = "media-policy-v4-20260601-"
BODY = """【服务器媒体保留规则更新】
此前通知中的媒体清理规则已更新,请以本通知为准:
- 5MiB 以内文件长期保留。
- 超过 5MiB 的文件,90 天后清理。
- 超过 100MiB 的超大文件 7 天后清理。
请自行保存重要大文件。"""然后运行:
/root/data/docker_data/synapse/scripts/send-media-policy-notice.py这样所有用户都会收到新版通知,而且上下文清楚。
确实要撤回旧通知
Matrix 里的撤回叫 redaction。它不会“编辑旧通知”,而是把旧事件内容抹掉。客户端通常会显示成“消息已删除”或“已撤回”。
要撤回旧通知,需要两个值:
event_id:发送 Server Notice 时返回的事件 ID,例如 $example
room_id:这条通知所在的通知房间 ID,例如 !abcdef:666666.xyz如果你只有 event_id,可以先从 Synapse 数据库查 room_id。示例使用 Docker 里的 PostgreSQL 容器 postgres16:
EVENT_ID='$这里填旧通知的event_id'
docker exec postgres16 psql -U synapse -d synapse -c "
SELECT event_id, room_id, sender, type
FROM events
WHERE event_id = '${EVENT_ID}';
"返回里会有 room_id。拿到 room_id 后,调用 Matrix redaction API:
TOKEN_FILE="/root/.config/synapse-admin/access_token"
TOKEN="$(cat "$TOKEN_FILE")"
ROOM_ID='!这里填room_id:666666.xyz'
EVENT_ID='$这里填旧通知的event_id'
ENC_ROOM_ID="$(python3 - <<PY
import urllib.parse
print(urllib.parse.quote('''${ROOM_ID}''', safe=''))
PY
)"
ENC_EVENT_ID="$(python3 - <<PY
import urllib.parse
print(urllib.parse.quote('''${EVENT_ID}''', safe=''))
PY
)"
TXN_ID="redact-media-policy-v1-$(date +%s)"
curl -sS -X PUT \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data '{"reason":"媒体保留规则通知已更新,请以新版通知为准"}' \
"http://127.0.0.1:8008/_matrix/client/v3/rooms/${ENC_ROOM_ID}/redact/${ENC_EVENT_ID}/${TXN_ID}"
echo如果你当时给 5 个用户都发了旧通知,就会有 5 个不同的 event_id。要撤回就得逐条查 room_id,逐条 redaction。
注意:
redaction 不能还原
redaction 不是物理硬删所有历史痕迹,而是移除事件内容
用户可能仍能看到“消息已删除/已撤回”的占位官方参考:
- Matrix Redaction API:https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidredacteventidtxnid
八、常见错误
M_UNKNOWN_TOKEN
示例:
{"errcode":"M_UNKNOWN_TOKEN","error":"Invalid access token passed.","soft_logout":false}说明 token 失效或 token 文件不对。重新登录管理员账号,刷新 /root/.config/synapse-admin/access_token。
403
token 能识别,但用户不是 Synapse 管理员。换成管理员账号的 token,或把专用账号设置为管理员。
M_NOT_FOUND
检查 Server Notices 系统账号时返回 M_NOT_FOUND 是正常的,说明账号未被占用。
如果发送通知时返回 M_NOT_FOUND,通常是目标用户 ID 写错了。
用户看不到通知房间
先确认 homeserver.yaml 里有:
server_notices:
auto_join: true然后确认 Synapse 已经重启:
docker ps --filter name=matrix-synapse --format 'table {{.Names}}\t{{.Status}}'再给单个测试用户发送一次,确认返回了 event_id。
九、生产实测结果
一次生产环境实测:
启用 server_notices 后重启 Synapse
目标用户:5 个
成功发送:5 个
失败:0 个
Synapse 容器状态:healthy这套做法适合媒体清理规则上线、隐私政策变更、维护公告等低频通知。不要用它刷屏;真的要做“每次上传大文件都提醒”,应该单独做上传检测模块或反代日志分析,不建议混在 Server Notices 公告脚本里。