가족이 Memos에 공유 메모를 남기면 디스코드 채널로 알림을 보내는 중계 컨테이너.
Memos에 웹훅 기능이 있지만 사설 IP로 향하는 URL을 거부한다 (SSRF 방지):
Create webhook: webhook URL must not resolve to a reserved or private IP address
이를 우회하려면 hook.jjang.synology.me 같은 공개 주소를 만들어야 하는데,
그러면 인터넷에서 아무나 POST해 가짜 알림을 넣을 수 있다.
폴링으로 바꾸니 오히려 나아졌다:
/volume1/docker/memo-notify/relay.mjs
const MEMOS = process.env.MEMOS_API ?? "http://192.168.0.240:5230";
const TOKEN = process.env.MEMOS_TOKEN;
const WEBHOOK = process.env.DISCORD_WEBHOOK;
const LINK = process.env.MEMOS_URL ?? "https://memo.jjang.synology.me";
const INTERVAL = Number(process.env.INTERVAL_SEC ?? 60) * 1000;
const SHOW_BODY = process.env.SHOW_BODY !== "false";
let lastSeen = Date.now(); // 시작 시점 이후의 메모만 알림
async function notify(memo) {
const author = String(memo.creator ?? "").replace("users/", "") || "누군가";
const body = String(memo.content ?? "").trim();
await fetch(WEBHOOK, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
username: "우리집 메모",
embeds: [{
title: `📝 ${author} 님이 메모를 남겼습니다`,
description: SHOW_BODY ? body.slice(0, 1500) : undefined,
url: LINK,
color: 0x1d7a5b,
timestamp: memo.createTime,
}],
}),
});
}
async function poll() {
try {
const res = await fetch(`${MEMOS}/api/v1/memos?pageSize=20`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
if (!res.ok) throw new Error(`Memos 응답 ${res.status}`);
const memos = (await res.json()).memos ?? [];
const fresh = memos
.filter((m) => m.visibility !== "PRIVATE") // 개인 메모는 알리지 않음
.filter((m) => Date.parse(m.createTime) > lastSeen)
.sort((a, b) => Date.parse(a.createTime) - Date.parse(b.createTime));
for (const memo of fresh) {
await notify(memo);
lastSeen = Math.max(lastSeen, Date.parse(memo.createTime));
}
} catch (err) {
console.error("poll error:", err.message);
}
}
console.log(`polling every ${INTERVAL / 1000}s`);
setInterval(poll, INTERVAL);
poll();
services:
memo-notify:
image: node:22-alpine
container_name: memo-notify
restart: unless-stopped
working_dir: /app
command: node relay.mjs
volumes:
- /volume1/docker/memo-notify:/app:ro
environment:
- MEMOS_API=http://192.168.0.240:5230
- MEMOS_TOKEN=<Memos Access Token>
- MEMOS_URL=https://memo.jjang.synology.me
- DISCORD_WEBHOOK=<디스코드 웹훅 URL>
- INTERVAL_SEC=60
- SHOW_BODY=true
:ro) 마운트라 권한 문제가 없다 (스크립트만 읽음)sudo docker logs -f memo-notify
# → polling every 60s
Memos에서 Protected 메모를 하나 쓰면 1분 안에 디스코드에 카드가 뜬다.
Private 메모로도 한 번 써서 알림이 오지 않는 것까지 확인할 것.
내용을 숨기고 싶을 때 (프라이버시)
SHOW_BODY=false
→ "○○ 님이 메모를 남겼습니다" 만 전송, 내용은 디스코드에 저장되지 않음
특정 태그만 알림
.filter((m) => String(m.content ?? "").includes("#긴급"))
컨테이너를 재시작하면 lastSeen 이 현재 시각으로 초기화된다.
재시작 중 올라온 메모는 알림이 가지 않는다 — 재시작마다 과거 메모가 쏟아지는 것보다 낫다는 판단.
웹 UI가 없어서 조용히 죽는다. Uptime Kuma에 Docker Container 모니터로 등록해 둘 것.