修复关注
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- Remote actors need a slash-free local id so Mastodon API paths like
|
||||||
|
-- /api/v1/accounts/:id/follow can target them without URL-encoding "/".
|
||||||
|
|
||||||
|
ALTER TABLE actor_cache ADD COLUMN local_id TEXT;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_actor_cache_local_id ON actor_cache(local_id);
|
||||||
@@ -147,9 +147,13 @@ npm run deploy
|
|||||||
这是一个单用户可运行实现,不是完整 Mastodon 服务端:
|
这是一个单用户可运行实现,不是完整 Mastodon 服务端:
|
||||||
|
|
||||||
- 只支持单管理员账号自动初始化,不开放注册
|
- 只支持单管理员账号自动初始化,不开放注册
|
||||||
- 远端嘟文缓存只在被本地账号关注的 actor 发出的 `Create(Note)` 时写入,不抓取历史 outbox
|
- 本地状态的可见性语义并不完整:
|
||||||
- `media_attachments` 已缓存(URL 指向远端原始域名),但 `mentions`、`tags` 在远端缓存嘟文中是空数组
|
- `visibility=private` 目前只会写入本地库,不会按 followers-only 语义完整投递
|
||||||
- 私信(direct visibility)的检索没有按收信人过滤,目前所有客户端都能在公开时间线之外读到自己的嘟文,不应当作私信使用
|
- `unlisted` / `private` / `direct` 没有做完整的读权限控制,`GET /api/v1/accounts/:id/statuses`、`GET /api/v1/statuses/:id`、搜索接口以及 ActivityPub outbox 都可能暴露非公开内容,不应当存放敏感或真正私密的信息
|
||||||
|
- 远端嘟文缓存只在被本地账号关注的 actor 发出的入站 `Create(Note)` 时写入,不抓取历史 outbox
|
||||||
|
- 远端缓存嘟文只保留正文、CW、语言和附件; `mentions`、`tags`、互动计数等不会完整恢复,在 home timeline 中统一按 `visibility: public` 返回
|
||||||
|
- 媒体上传只支持 `image/jpeg`、`image/png`、`image/gif`、`image/webp`,单文件 10MB,单条状态最多 4 个附件; 头像和封面同样只按图片路径处理
|
||||||
|
- 没有实现接口级限流、反滥用或审核流; `follow_requests` 相关接口仍是 stub
|
||||||
- 没有实现轮询(poll)、列表(list)、推送(push)、未来嘟文(scheduled)等
|
- 没有实现轮询(poll)、列表(list)、推送(push)、未来嘟文(scheduled)等
|
||||||
|
|
||||||
## 参考
|
## 参考
|
||||||
|
|||||||
@@ -153,10 +153,13 @@ export async function upsertActorCache(env: Env, actor: RemoteActor): Promise<Ac
|
|||||||
const sharedInbox = actor.endpoints?.sharedInbox ?? null;
|
const sharedInbox = actor.endpoints?.sharedInbox ?? null;
|
||||||
const iconUrl = typeof actor.icon === "string" ? actor.icon : actor.icon?.url ?? null;
|
const iconUrl = typeof actor.icon === "string" ? actor.icon : actor.icon?.url ?? null;
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const existing = await getActorFromCache(env, actor.id);
|
||||||
|
const localId = existing?.local_id ?? id();
|
||||||
await env.DB.prepare(
|
await env.DB.prepare(
|
||||||
`INSERT INTO actor_cache (id, inbox, shared_inbox, preferred_username, name, summary, icon_url, public_key_id, public_key_pem, fetched_at)
|
`INSERT INTO actor_cache (id, local_id, inbox, shared_inbox, preferred_username, name, summary, icon_url, public_key_id, public_key_pem, fetched_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
local_id = COALESCE(actor_cache.local_id, excluded.local_id),
|
||||||
inbox = excluded.inbox,
|
inbox = excluded.inbox,
|
||||||
shared_inbox = excluded.shared_inbox,
|
shared_inbox = excluded.shared_inbox,
|
||||||
preferred_username = excluded.preferred_username,
|
preferred_username = excluded.preferred_username,
|
||||||
@@ -169,6 +172,7 @@ export async function upsertActorCache(env: Env, actor: RemoteActor): Promise<Ac
|
|||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
actor.id,
|
actor.id,
|
||||||
|
localId,
|
||||||
inbox,
|
inbox,
|
||||||
sharedInbox,
|
sharedInbox,
|
||||||
actor.preferredUsername ?? null,
|
actor.preferredUsername ?? null,
|
||||||
@@ -183,6 +187,17 @@ export async function upsertActorCache(env: Env, actor: RemoteActor): Promise<Ac
|
|||||||
return getActorFromCache(env, actor.id);
|
return getActorFromCache(env, actor.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getActorByLocalId(env: Env, localId: string): Promise<ActorCache | null> {
|
||||||
|
return env.DB.prepare("SELECT * FROM actor_cache WHERE local_id = ?").bind(localId).first<ActorCache>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureActorLocalId(env: Env, cache: ActorCache): Promise<ActorCache> {
|
||||||
|
if (cache.local_id) return cache;
|
||||||
|
const localId = id();
|
||||||
|
await env.DB.prepare("UPDATE actor_cache SET local_id = COALESCE(local_id, ?) WHERE id = ?").bind(localId, cache.id).run();
|
||||||
|
return await getActorFromCache(env, cache.id) ?? { ...cache, local_id: localId };
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteActorFromCache(env: Env, actorId: string): Promise<void> {
|
export async function deleteActorFromCache(env: Env, actorId: string): Promise<void> {
|
||||||
await env.DB.prepare("DELETE FROM actor_cache WHERE id = ?").bind(actorId).run();
|
await env.DB.prepare("DELETE FROM actor_cache WHERE id = ?").bind(actorId).run();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -7,6 +7,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
actorCacheStale,
|
actorCacheStale,
|
||||||
deleteActorFromCache,
|
deleteActorFromCache,
|
||||||
|
ensureActorLocalId,
|
||||||
getActorByKeyId,
|
getActorByKeyId,
|
||||||
getActorFromCache,
|
getActorFromCache,
|
||||||
recordNotification,
|
recordNotification,
|
||||||
@@ -21,9 +22,9 @@ const ACTIVITY_HEADERS = "application/activity+json, application/ld+json; profil
|
|||||||
export async function resolveRemoteActor(env: Env, actorId: string, opts: { force?: boolean } = {}): Promise<ActorCache | null> {
|
export async function resolveRemoteActor(env: Env, actorId: string, opts: { force?: boolean } = {}): Promise<ActorCache | null> {
|
||||||
if (!actorId) return null;
|
if (!actorId) return null;
|
||||||
const cached = await getActorFromCache(env, actorId);
|
const cached = await getActorFromCache(env, actorId);
|
||||||
if (cached && !opts.force && !actorCacheStale(cached)) return cached;
|
if (cached && !opts.force && !actorCacheStale(cached)) return ensureActorLocalId(env, cached);
|
||||||
const fetched = await fetchRemoteActor(actorId);
|
const fetched = await fetchRemoteActor(actorId);
|
||||||
if (!fetched) return cached;
|
if (!fetched) return cached ? ensureActorLocalId(env, cached) : null;
|
||||||
return upsertActorCache(env, fetched);
|
return upsertActorCache(env, fetched);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { ensureAdminUser } from "./db";
|
|||||||
import { HttpError, cors, json, svgResponse } from "./http";
|
import { HttpError, cors, json, svgResponse } from "./http";
|
||||||
import {
|
import {
|
||||||
accountStatuses,
|
accountStatuses,
|
||||||
|
accountFollowers,
|
||||||
|
accountFollowing,
|
||||||
authorize,
|
authorize,
|
||||||
authorizeFollowRequest,
|
authorizeFollowRequest,
|
||||||
authorizePage,
|
authorizePage,
|
||||||
@@ -115,6 +117,8 @@ async function route(request: Request, env: Env): Promise<Response> {
|
|||||||
|
|
||||||
if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)$/))) return getAccount(env, decodeURIComponent(m[1]));
|
if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)$/))) return getAccount(env, decodeURIComponent(m[1]));
|
||||||
if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/statuses$/))) return accountStatuses(request, env, decodeURIComponent(m[1]));
|
if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/statuses$/))) return accountStatuses(request, env, decodeURIComponent(m[1]));
|
||||||
|
if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/followers$/))) return accountFollowers(request, env, decodeURIComponent(m[1]));
|
||||||
|
if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/following$/))) return accountFollowing(request, env, decodeURIComponent(m[1]));
|
||||||
if (method === "POST" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/follow$/))) return followAccount(request, env, decodeURIComponent(m[1]));
|
if (method === "POST" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/follow$/))) return followAccount(request, env, decodeURIComponent(m[1]));
|
||||||
if (method === "POST" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/unfollow$/))) return unfollowAccount(request, env, decodeURIComponent(m[1]));
|
if (method === "POST" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/unfollow$/))) return unfollowAccount(request, env, decodeURIComponent(m[1]));
|
||||||
|
|
||||||
|
|||||||
+120
-14
@@ -21,6 +21,8 @@ import {
|
|||||||
findOutgoingFollow,
|
findOutgoingFollow,
|
||||||
findPin,
|
findPin,
|
||||||
findReblog,
|
findReblog,
|
||||||
|
getActorByLocalId,
|
||||||
|
getActorFromCache,
|
||||||
getAdminUser,
|
getAdminUser,
|
||||||
getAppByClientId,
|
getAppByClientId,
|
||||||
getStatus,
|
getStatus,
|
||||||
@@ -55,6 +57,7 @@ import {
|
|||||||
} from "./http";
|
} from "./http";
|
||||||
import type { ParsedBody } from "./http";
|
import type { ParsedBody } from "./http";
|
||||||
import type {
|
import type {
|
||||||
|
ActorCache,
|
||||||
CachedStatus,
|
CachedStatus,
|
||||||
Follow,
|
Follow,
|
||||||
Media,
|
Media,
|
||||||
@@ -416,6 +419,8 @@ function extractFieldsAttributes(body: ParsedBody): { name: string; value: strin
|
|||||||
export async function getAccount(env: Env, accountId: string): Promise<Response> {
|
export async function getAccount(env: Env, accountId: string): Promise<Response> {
|
||||||
const local = await getUserByIdOrUsername(env, accountId);
|
const local = await getUserByIdOrUsername(env, accountId);
|
||||||
if (local) return json(await accountJson(env, local));
|
if (local) return json(await accountJson(env, local));
|
||||||
|
const byLocalId = await getActorByLocalId(env, accountId);
|
||||||
|
if (byLocalId) return json(remoteAccountJson(byLocalId));
|
||||||
if (accountId.startsWith("http://") || accountId.startsWith("https://")) {
|
if (accountId.startsWith("http://") || accountId.startsWith("https://")) {
|
||||||
const cache = await resolveRemoteActor(env, accountId);
|
const cache = await resolveRemoteActor(env, accountId);
|
||||||
if (cache) return json(remoteAccountJson(cache));
|
if (cache) return json(remoteAccountJson(cache));
|
||||||
@@ -441,20 +446,90 @@ export async function lookupAccount(request: Request, env: Env): Promise<Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function accountStatuses(request: Request, env: Env, accountId: string): Promise<Response> {
|
export async function accountStatuses(request: Request, env: Env, accountId: string): Promise<Response> {
|
||||||
const user = await getUserByIdOrUsername(env, accountId);
|
|
||||||
if (!user) return json({ error: "Record not found" }, 404);
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const limit = clampLimit(url.searchParams.get("limit"), 20, 40);
|
const limit = clampLimit(url.searchParams.get("limit"), 20, 40);
|
||||||
const excludeReplies = url.searchParams.get("exclude_replies") === "true";
|
|
||||||
const where: string[] = ["user_id = ?"];
|
const user = await getUserByIdOrUsername(env, accountId);
|
||||||
|
if (user) {
|
||||||
|
const excludeReplies = url.searchParams.get("exclude_replies") === "true";
|
||||||
|
const where: string[] = ["user_id = ?"];
|
||||||
|
const binds: unknown[] = [user.id];
|
||||||
|
if (excludeReplies) where.push("in_reply_to_id IS NULL");
|
||||||
|
pagedAppend(where, binds, url);
|
||||||
|
const sql = `SELECT * FROM statuses WHERE ${where.join(" AND ")} ORDER BY created_at DESC LIMIT ?`;
|
||||||
|
binds.push(limit);
|
||||||
|
const rows = await env.DB.prepare(sql).bind(...binds).all<Status>();
|
||||||
|
const items = await serializeStatuses(env, rows.results, request, new Map([[user.id, user]]));
|
||||||
|
return withPagination(json(items), request, rows.results.map((row) => row.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const remote = await getActorByLocalId(env, accountId)
|
||||||
|
?? (accountId.startsWith("http://") || accountId.startsWith("https://") ? await resolveRemoteActor(env, accountId) : null);
|
||||||
|
if (remote) {
|
||||||
|
const rows = await env.DB.prepare(
|
||||||
|
"SELECT * FROM cached_statuses WHERE actor = ? ORDER BY published DESC LIMIT ?"
|
||||||
|
).bind(remote.id, limit).all<CachedStatus>();
|
||||||
|
const items = await Promise.all(rows.results.map((row) => cachedStatusToMastodon(env, row)));
|
||||||
|
return json(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ error: "Record not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function accountFollowers(request: Request, env: Env, accountId: string): Promise<Response> {
|
||||||
|
const user = await getUserByIdOrUsername(env, accountId);
|
||||||
|
if (!user) return remoteAccountListFallback(env, accountId);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const limit = clampLimit(url.searchParams.get("limit"), 20, 80);
|
||||||
|
const where: string[] = ["local_user_id = ?", "accepted = 1"];
|
||||||
const binds: unknown[] = [user.id];
|
const binds: unknown[] = [user.id];
|
||||||
if (excludeReplies) where.push("in_reply_to_id IS NULL");
|
pagedAppendForTable(where, binds, url, "follows");
|
||||||
pagedAppend(where, binds, url);
|
const rows = await env.DB.prepare(
|
||||||
const sql = `SELECT * FROM statuses WHERE ${where.join(" AND ")} ORDER BY created_at DESC LIMIT ?`;
|
`SELECT * FROM follows WHERE ${where.join(" AND ")} ORDER BY created_at DESC LIMIT ?`
|
||||||
binds.push(limit);
|
).bind(...binds, limit).all<Follow>();
|
||||||
const rows = await env.DB.prepare(sql).bind(...binds).all<Status>();
|
|
||||||
const items = await serializeStatuses(env, rows.results, request, new Map([[user.id, user]]));
|
const accounts = await actorIdsToAccounts(env, rows.results.map((row) => row.follower_actor));
|
||||||
return withPagination(json(items), request, rows.results.map((row) => row.id));
|
return withPagination(json(accounts), request, rows.results.map((row) => row.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function accountFollowing(request: Request, env: Env, accountId: string): Promise<Response> {
|
||||||
|
const user = await getUserByIdOrUsername(env, accountId);
|
||||||
|
if (!user) return remoteAccountListFallback(env, accountId);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const limit = clampLimit(url.searchParams.get("limit"), 20, 80);
|
||||||
|
const where: string[] = ["local_user_id = ?", "accepted = 1"];
|
||||||
|
const binds: unknown[] = [user.id];
|
||||||
|
pagedAppendForTable(where, binds, url, "outgoing_follows");
|
||||||
|
const rows = await env.DB.prepare(
|
||||||
|
`SELECT * FROM outgoing_follows WHERE ${where.join(" AND ")} ORDER BY created_at DESC LIMIT ?`
|
||||||
|
).bind(...binds, limit).all<{ id: string; target_actor: string }>();
|
||||||
|
|
||||||
|
const accounts = await actorIdsToAccounts(env, rows.results.map((row) => row.target_actor));
|
||||||
|
return withPagination(json(accounts), request, rows.results.map((row) => row.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remoteAccountListFallback(env: Env, accountId: string): Promise<Response> {
|
||||||
|
const remote = await getActorByLocalId(env, accountId)
|
||||||
|
?? (accountId.startsWith("http://") || accountId.startsWith("https://") ? await resolveRemoteActor(env, accountId) : null);
|
||||||
|
if (remote) return json([]);
|
||||||
|
return json({ error: "Record not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function actorIdsToAccounts(env: Env, actorIds: string[]): Promise<Record<string, unknown>[]> {
|
||||||
|
const accounts = await Promise.all(actorIds.map((actorId) => accountFromActorId(env, actorId)));
|
||||||
|
return accounts.filter((account): account is Record<string, unknown> => Boolean(account));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function accountFromActorId(env: Env, actorId: string): Promise<Record<string, unknown> | null> {
|
||||||
|
if (actorId.startsWith(baseUrl(env))) {
|
||||||
|
const match = actorId.match(/\/users\/([^/?#]+)$/);
|
||||||
|
const user = match ? await getUserByUsername(env, match[1]) : null;
|
||||||
|
return user ? accountJson(env, user) : null;
|
||||||
|
}
|
||||||
|
const cache = await resolveRemoteActor(env, actorId) ?? await getActorFromCache(env, actorId);
|
||||||
|
return cache ? remoteAccountJson(cache) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createStatus(request: Request, env: Env): Promise<Response> {
|
export async function createStatus(request: Request, env: Env): Promise<Response> {
|
||||||
@@ -1305,11 +1380,11 @@ async function accountJson(env: Env, user: User): Promise<Record<string, unknown
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteAccountJson(cache: { id: string; preferred_username: string | null; name: string | null; summary: string | null; icon_url: string | null; fetched_at: string }): Record<string, unknown> {
|
function remoteAccountJson(cache: ActorCache): Record<string, unknown> {
|
||||||
const host = (() => { try { return new URL(cache.id).host; } catch { return "remote"; } })();
|
const host = (() => { try { return new URL(cache.id).host; } catch { return "remote"; } })();
|
||||||
const username = cache.preferred_username ?? cache.id.split("/").pop() ?? "user";
|
const username = cache.preferred_username ?? cache.id.split("/").pop() ?? "user";
|
||||||
return {
|
return {
|
||||||
id: cache.id,
|
id: cache.local_id ?? cache.id,
|
||||||
username,
|
username,
|
||||||
acct: `${username}@${host}`,
|
acct: `${username}@${host}`,
|
||||||
display_name: cache.name ?? username,
|
display_name: cache.name ?? username,
|
||||||
@@ -1376,7 +1451,19 @@ type AccountTarget = { kind: "local"; userId: string; actorId: string } | { kind
|
|||||||
async function resolveAccountTarget(env: Env, key: string): Promise<AccountTarget | null> {
|
async function resolveAccountTarget(env: Env, key: string): Promise<AccountTarget | null> {
|
||||||
const local = await getUserByIdOrUsername(env, key);
|
const local = await getUserByIdOrUsername(env, key);
|
||||||
if (local) return { kind: "local", userId: local.id, actorId: actorUrl(env, local) };
|
if (local) return { kind: "local", userId: local.id, actorId: actorUrl(env, local) };
|
||||||
if (key.startsWith("http://") || key.startsWith("https://")) return { kind: "remote", actorId: key };
|
|
||||||
|
const byLocalId = await getActorByLocalId(env, key);
|
||||||
|
if (byLocalId) return { kind: "remote", actorId: byLocalId.id };
|
||||||
|
|
||||||
|
if (key.startsWith("http://") || key.startsWith("https://")) {
|
||||||
|
if (key.startsWith(baseUrl(env))) {
|
||||||
|
const match = key.match(/\/users\/([^/?#]+)$/);
|
||||||
|
const u = match ? await getUserByUsername(env, match[1]) : null;
|
||||||
|
if (u) return { kind: "local", userId: u.id, actorId: actorUrl(env, u) };
|
||||||
|
}
|
||||||
|
await resolveRemoteActor(env, key);
|
||||||
|
return { kind: "remote", actorId: key };
|
||||||
|
}
|
||||||
if (key.includes("@")) {
|
if (key.includes("@")) {
|
||||||
const resolved = await resolveAcct(env, key);
|
const resolved = await resolveAcct(env, key);
|
||||||
if (!resolved) return null;
|
if (!resolved) return null;
|
||||||
@@ -1385,6 +1472,7 @@ async function resolveAccountTarget(env: Env, key: string): Promise<AccountTarge
|
|||||||
const localUser = match ? await getUserByUsername(env, match[1]) : null;
|
const localUser = match ? await getUserByUsername(env, match[1]) : null;
|
||||||
if (localUser) return { kind: "local", userId: localUser.id, actorId: resolved.actorId };
|
if (localUser) return { kind: "local", userId: localUser.id, actorId: resolved.actorId };
|
||||||
}
|
}
|
||||||
|
await resolveRemoteActor(env, resolved.actorId);
|
||||||
return { kind: "remote", actorId: resolved.actorId };
|
return { kind: "remote", actorId: resolved.actorId };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -1589,6 +1677,24 @@ function pagedAppend(where: string[], binds: unknown[], url: URL): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pagedAppendForTable(where: string[], binds: unknown[], url: URL, table: "follows" | "outgoing_follows"): void {
|
||||||
|
const maxId = url.searchParams.get("max_id");
|
||||||
|
if (maxId) {
|
||||||
|
where.push(`created_at < (SELECT created_at FROM ${table} WHERE id = ?)`);
|
||||||
|
binds.push(maxId);
|
||||||
|
}
|
||||||
|
const sinceId = url.searchParams.get("since_id");
|
||||||
|
if (sinceId) {
|
||||||
|
where.push(`created_at > (SELECT created_at FROM ${table} WHERE id = ?)`);
|
||||||
|
binds.push(sinceId);
|
||||||
|
}
|
||||||
|
const minId = url.searchParams.get("min_id");
|
||||||
|
if (minId) {
|
||||||
|
where.push(`created_at > (SELECT created_at FROM ${table} WHERE id = ?)`);
|
||||||
|
binds.push(minId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function withPagination(response: Response, request: Request, ids: string[]): Response {
|
function withPagination(response: Response, request: Request, ids: string[]): Response {
|
||||||
if (ids.length === 0) return response;
|
if (ids.length === 0) return response;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export type OAuthToken = {
|
|||||||
|
|
||||||
export type ActorCache = {
|
export type ActorCache = {
|
||||||
id: string;
|
id: string;
|
||||||
|
local_id: string | null;
|
||||||
inbox: string;
|
inbox: string;
|
||||||
shared_inbox: string | null;
|
shared_inbox: string | null;
|
||||||
preferred_username: string | null;
|
preferred_username: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user