From 5425f93d1f856f257e979118d0c9bc359f05153b Mon Sep 17 00:00:00 2001 From: tangsongdayo Date: Sun, 19 Jul 2026 04:48:21 +0800 Subject: [PATCH] fix: sync only API key owner public memos --- app/components/post-card.tsx | 4 ++-- app/page.tsx | 2 +- app/sources/[id]/page.tsx | 2 +- app/tags/[tag]/page.tsx | 2 +- lib/memos.ts | 5 +++-- worker/index.ts | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/components/post-card.tsx b/app/components/post-card.tsx index a404be2..43c1060 100644 --- a/app/components/post-card.tsx +++ b/app/components/post-card.tsx @@ -3,9 +3,9 @@ import { Attachments } from "./attachments"; import { Markdown } from "./markdown"; import { canonicalTags } from "@/lib/tags"; -export type PublicPost = { id: number; source_id: number | null; content: string; tags_json: string; attachments_json: string; created_at: string; remote_created_at?: string | null; username: string; name: string | null; source_base_url: string | null; comment_count: number; reaction_count: number }; +export type PublicPost = { id: number; source_id: number | null; origin: string; content: string; tags_json: string; attachments_json: string; created_at: string; remote_created_at?: string | null; username: string; name: string | null; remote_display_name?: string | null; source_base_url: string | null; comment_count: number; reaction_count: number }; export function PostCard({ post }: { post: PublicPost }) { let tags: string[] = []; try { tags = canonicalTags(JSON.parse(post.tags_json)); } catch { /* Ignore malformed legacy tags. */ } - const publishedAt = post.remote_created_at || post.created_at; return
@{post.username}{post.name ? ` · ${post.name}` : ""}{new Date(publishedAt).toLocaleString("zh-TW")}
{tags.map((tag) => #{tag})}{post.source_id && 來源}閱讀全文 · 💬 {post.comment_count} 🙂 {post.reaction_count}
; + const publishedAt = post.remote_created_at || post.created_at; const author = post.origin === "memos" ? (post.remote_display_name || post.name || post.username) : post.username; return
@{author}{post.name ? ` · ${post.name}` : ""}{new Date(publishedAt).toLocaleString("zh-TW")}
{tags.map((tag) => #{tag})}{post.source_id && 來源}閱讀全文 · 💬 {post.comment_count} 🙂 {post.reaction_count}
; } diff --git a/app/page.tsx b/app/page.tsx index 957905b..6047923 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,7 +12,7 @@ export default async function Home({ searchParams }: { searchParams: Promise= date(?)"); args.push(from); } if (to) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) <= date(?)"); args.push(to); } if (attachments) where.push("p.attachments_json <> '[]'"); const joins = " FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id "; const predicate = ` WHERE ${where.join(" AND ")}`; const total = Number((db.prepare(`SELECT count(*) count${joins}${predicate}`).get(...args) as { count: number }).count); const pages = Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(page, pages); - const posts = db.prepare(`SELECT p.*,u.username,s.name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count${joins}${predicate} ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT ? OFFSET ?`).all(...args, pageSize, (safePage - 1) * pageSize) as PublicPost[]; + const posts = db.prepare(`SELECT p.*,u.username,s.name,s.remote_display_name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count${joins}${predicate} ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT ? OFFSET ?`).all(...args, pageSize, (safePage - 1) * pageSize) as PublicPost[]; const sources = db.prepare("SELECT id,name FROM sources WHERE is_enabled=1 ORDER BY name").all() as { id: number; name: string }[]; const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) if (value && key !== "page") params.set(key, value); const pageHref = (target: number) => { const next = new URLSearchParams(params); next.set("page", String(target)); return `/?${next}`; }; return <>

公開 Memos Hub

聚合朋友們公開分享的筆記。

發佈/連接來源

共 {total} 篇公開貼文

{posts.length ? posts.map((post) => ) :

尚無符合的公開貼文。

}{pages > 1 && }; diff --git a/app/sources/[id]/page.tsx b/app/sources/[id]/page.tsx index 3f21b6b..1b586aa 100644 --- a/app/sources/[id]/page.tsx +++ b/app/sources/[id]/page.tsx @@ -6,6 +6,6 @@ import { PostCard, type PublicPost } from "@/app/components/post-card"; export const dynamic = "force-dynamic"; export default async function SourcePage({ params }: { params: Promise<{ id: string }> }) { const { id: rawId } = await params; const id = Number(rawId); const source = db.prepare("SELECT id,name,base_url,remote_display_name,remote_avatar_url FROM sources WHERE id=?").get(id) as { id: number; name: string; base_url: string; remote_display_name: string | null; remote_avatar_url: string | null } | undefined; if (!source) notFound(); - const posts = db.prepare("SELECT p.*,u.username,s.name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.source_id=? AND p.visibility='PUBLIC' AND p.hidden=0 ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT 100").all(id) as PublicPost[]; + const posts = db.prepare("SELECT p.*,u.username,s.name,s.remote_display_name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.source_id=? AND p.visibility='PUBLIC' AND p.hidden=0 ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT 100").all(id) as PublicPost[]; return <>

← 探索

{source.name}

{source.remote_avatar_url && } {source.remote_display_name || "Memos"}
{source.base_url} · {posts.length} 篇公開貼文

{posts.map((post) => )}; } diff --git a/app/tags/[tag]/page.tsx b/app/tags/[tag]/page.tsx index 8854c8a..ad36ad6 100644 --- a/app/tags/[tag]/page.tsx +++ b/app/tags/[tag]/page.tsx @@ -6,6 +6,6 @@ import { PostCard, type PublicPost } from "@/app/components/post-card"; export const dynamic = "force-dynamic"; export default async function TagPage({ params }: { params: Promise<{ tag: string }> }) { const { tag: encoded } = await params; const tag = decodeURIComponent(encoded).trim(); if (!tag) notFound(); - const posts = db.prepare("SELECT p.*,u.username,s.name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.visibility='PUBLIC' AND p.hidden=0 AND p.tags_json LIKE ? ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT 100").all(`%${JSON.stringify(tag).slice(1, -1)}%`) as PublicPost[]; + const posts = db.prepare("SELECT p.*,u.username,s.name,s.remote_display_name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.visibility='PUBLIC' AND p.hidden=0 AND p.tags_json LIKE ? ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT 100").all(`%${JSON.stringify(tag).slice(1, -1)}%`) as PublicPost[]; return <>

← 探索

#{tag}

{posts.length} 篇公開貼文

{posts.map((post) => )}; } diff --git a/lib/memos.ts b/lib/memos.ts index 2cddf9d..5222ef3 100644 --- a/lib/memos.ts +++ b/lib/memos.ts @@ -1,6 +1,6 @@ -export type MemosMemo = { name: string; content: string; visibility: string; createTime?: string; updateTime?: string; tags?: string[]; attachments?: { type?: string }[]; resources?: { type?: string }[] }; +export type MemosMemo = { name: string; content: string; visibility: string; creator?: string; createTime?: string; updateTime?: string; tags?: string[]; attachments?: { type?: string }[]; resources?: { type?: string }[] }; export type MemosIdentity = { name: string; username?: string; nickname?: string; avatarUrl?: string; avatar?: string }; -export type MemosSyncRules = { tags?: string[]; from?: string | null; to?: string | null; attachmentMode?: "all" | "images" | "none" }; +export type MemosSyncRules = { creator?: string | null; tags?: string[]; from?: string | null; to?: string | null; attachmentMode?: "all" | "images" | "none" }; const base = (url: string) => url.replace(/\/+$/, "") + "/api/v1"; async function request(url: string, token: string, init?: RequestInit) { const res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(init?.headers || {}) }, cache: "no-store" }); @@ -19,6 +19,7 @@ export async function listMemos(baseUrl: string, token: string, rules: MemosSync const tags = rules.tags?.filter(Boolean) || []; const mode = rules.attachmentMode || "all"; return all.filter((memo) => { if (memo.visibility !== "PUBLIC") return false; + if (rules.creator && memo.creator !== rules.creator) return false; if (tags.length && !tags.every((tag) => memo.tags?.includes(tag))) return false; const created = memo.createTime?.slice(0, 10); if (rules.from && (!created || created < rules.from)) return false; if (rules.to && (!created || created > rules.to)) return false; return true; diff --git a/worker/index.ts b/worker/index.ts index 212ab0e..393da21 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -6,7 +6,7 @@ import { decrypt } from "../lib/crypto"; import { createMemo, createRemoteFile, getMemosIdentity, listMemos, memoUrl, setMemoAttachments } from "../lib/memos"; import { recordError } from "../lib/observability"; -type Source = { id: number; user_id: number; base_url: string; token_encrypted: string; is_enabled: number; sync_tags_json: string; sync_from: string | null; sync_to: string | null; sync_attachment_mode: "all" | "images" | "none"; attachment_storage_mode: "remote" | "images" | "all"; attachment_cache_limit_bytes: number }; +type Source = { id: number; user_id: number; base_url: string; token_encrypted: string; remote_user: string | null; is_enabled: number; sync_tags_json: string; sync_from: string | null; sync_to: string | null; sync_attachment_mode: "all" | "images" | "none"; attachment_storage_mode: "remote" | "images" | "all"; attachment_cache_limit_bytes: number }; type Job = { id: number; source_id: number; kind: "pull" | "push"; payload_json: string | null; attempts: number }; function remoteAttachmentUrl(attachment: any, baseUrl: string) { @@ -48,7 +48,7 @@ async function upsertRemote(source: Source, memo: any) { } async function pull(source: Source) { - const token = decrypt(source.token_encrypted); const rules = { tags: JSON.parse(source.sync_tags_json || "[]") as string[], from: source.sync_from, to: source.sync_to, attachmentMode: source.sync_attachment_mode }; + const token = decrypt(source.token_encrypted); const rules = { creator: source.remote_user, tags: JSON.parse(source.sync_tags_json || "[]") as string[], from: source.sync_from, to: source.sync_to, attachmentMode: source.sync_attachment_mode }; const [memos, identity] = await Promise.all([listMemos(source.base_url, token, rules), getMemosIdentity(source.base_url, token)]); for (const memo of memos) await upsertRemote(source, memo); const names = memos.map((memo) => memo.name);