附件上传限制
This commit is contained in:
+30
-8
@@ -157,7 +157,9 @@ export async function outbox(request: Request, env: Env, username: string): Prom
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
const url = new URL(request.url);
|
||||
const wantsPage = url.searchParams.has("page");
|
||||
const totalRow = await env.DB.prepare("SELECT COUNT(*) AS count FROM statuses WHERE user_id = ?").bind(user.id).first<{ count: number }>();
|
||||
const totalRow = await env.DB.prepare(
|
||||
"SELECT COUNT(*) AS count FROM statuses WHERE user_id = ? AND visibility IN ('public', 'unlisted')"
|
||||
).bind(user.id).first<{ count: number }>();
|
||||
const totalItems = totalRow?.count ?? 0;
|
||||
const base = `${actorUrl(env, user)}/outbox`;
|
||||
|
||||
@@ -172,7 +174,9 @@ export async function outbox(request: Request, env: Env, username: string): Prom
|
||||
}
|
||||
|
||||
const limit = clampLimit(url.searchParams.get("limit"), 20, 40);
|
||||
const rows = await env.DB.prepare("SELECT * FROM statuses WHERE user_id = ? ORDER BY created_at DESC LIMIT ?").bind(user.id, limit).all<Status>();
|
||||
const rows = await env.DB.prepare(
|
||||
"SELECT * FROM statuses WHERE user_id = ? AND visibility IN ('public', 'unlisted') ORDER BY created_at DESC LIMIT ?"
|
||||
).bind(user.id, limit).all<Status>();
|
||||
const items = rows.results.map((status) => createActivity(env, user, status));
|
||||
return activityJson({
|
||||
"@context": ACTIVITY_CONTEXT,
|
||||
@@ -211,6 +215,7 @@ export async function followingCollection(env: Env, username: string): Promise<R
|
||||
export async function activityObject(env: Env, objectId: string): Promise<Response> {
|
||||
const status = await getStatus(env, objectId);
|
||||
if (status) {
|
||||
if (status.visibility !== "public" && status.visibility !== "unlisted") return json({ error: "not_found" }, 404);
|
||||
const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(status.user_id).first<User>();
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
return activityJson(noteObject(env, user, status));
|
||||
@@ -513,8 +518,9 @@ async function localUserFromTarget(env: Env, actorId: string | null): Promise<Us
|
||||
}
|
||||
|
||||
export function createActivity(env: Env, user: User, status: Status, extra: { to?: string[]; cc?: string[] } = {}): Json {
|
||||
const to = extra.to ?? [PUBLIC_COLLECTION];
|
||||
const cc = extra.cc ?? [`${actorUrl(env, user)}/followers`];
|
||||
const audience = statusAudience(env, user, status);
|
||||
const to = extra.to ?? audience.to;
|
||||
const cc = extra.cc ?? audience.cc;
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: status.activity_id,
|
||||
@@ -527,13 +533,28 @@ export function createActivity(env: Env, user: User, status: Status, extra: { to
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteActivity(env: Env, user: User, status: Status): Json {
|
||||
function statusAudience(env: Env, user: User, status: Status): { to: string[]; cc: string[] } {
|
||||
if (status.visibility === "unlisted") {
|
||||
return { to: [`${actorUrl(env, user)}/followers`], cc: [PUBLIC_COLLECTION] };
|
||||
}
|
||||
if (status.visibility === "private") {
|
||||
return { to: [`${actorUrl(env, user)}/followers`], cc: [] };
|
||||
}
|
||||
if (status.visibility === "direct") {
|
||||
return { to: [], cc: [] };
|
||||
}
|
||||
return { to: [PUBLIC_COLLECTION], cc: [`${actorUrl(env, user)}/followers`] };
|
||||
}
|
||||
|
||||
export function deleteActivity(env: Env, user: User, status: Status, extra: { to?: string[]; cc?: string[] } = {}): Json {
|
||||
const audience = statusAudience(env, user, status);
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityUrl(env, id()),
|
||||
type: "Delete",
|
||||
actor: actorUrl(env, user),
|
||||
to: [PUBLIC_COLLECTION],
|
||||
to: extra.to ?? audience.to,
|
||||
cc: extra.cc ?? audience.cc,
|
||||
object: {
|
||||
id: status.object_id,
|
||||
type: "Tombstone"
|
||||
@@ -596,6 +617,7 @@ export function updatePersonActivity(env: Env, user: User, doc: Json): Json {
|
||||
}
|
||||
|
||||
export function noteObject(env: Env, user: User, status: Status, opts: { to?: string[]; cc?: string[]; attachments?: Json[]; tag?: Json[] } = {}): Json {
|
||||
const audience = statusAudience(env, user, status);
|
||||
return {
|
||||
id: status.object_id,
|
||||
type: "Note",
|
||||
@@ -606,8 +628,8 @@ export function noteObject(env: Env, user: User, status: Status, opts: { to?: st
|
||||
content: status.content,
|
||||
published: status.created_at,
|
||||
url: status.url,
|
||||
to: opts.to ?? [PUBLIC_COLLECTION],
|
||||
cc: opts.cc ?? [`${actorUrl(env, user)}/followers`],
|
||||
to: opts.to ?? audience.to,
|
||||
cc: opts.cc ?? audience.cc,
|
||||
attachment: opts.attachments ?? [],
|
||||
tag: opts.tag ?? []
|
||||
};
|
||||
|
||||
+126
-38
@@ -86,10 +86,18 @@ import {
|
||||
|
||||
const TOKEN_TTL_SECONDS = 60 * 60 * 24 * 90;
|
||||
const MAX_STATUS_CHARS = 5000;
|
||||
const MAX_MEDIA_ATTACHMENTS = 4;
|
||||
const REPORTED_MEDIA_ATTACHMENTS_LIMIT = 9999;
|
||||
const MAX_MEDIA_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
const SUPPORTED_MIME = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||
const VALID_STATUS_VISIBILITIES = new Set(["public", "unlisted", "private", "direct"]);
|
||||
|
||||
type StatusVisibility = "public" | "unlisted" | "private" | "direct";
|
||||
type StatusViewer = {
|
||||
user: User | null;
|
||||
actor: string | null;
|
||||
followsByOwnerId: Map<string, boolean>;
|
||||
};
|
||||
|
||||
function parseRedirectUris(value: string): string[] {
|
||||
return value.split(/\s+/).map((item) => item.trim()).filter(Boolean);
|
||||
@@ -120,7 +128,7 @@ export async function instance(env: Env): Promise<Response> {
|
||||
approval_required: false,
|
||||
invites_enabled: false,
|
||||
configuration: {
|
||||
statuses: { max_characters: MAX_STATUS_CHARS, max_media_attachments: MAX_MEDIA_ATTACHMENTS, characters_reserved_per_url: 23 },
|
||||
statuses: { max_characters: MAX_STATUS_CHARS, max_media_attachments: REPORTED_MEDIA_ATTACHMENTS_LIMIT, characters_reserved_per_url: 23 },
|
||||
media_attachments: { supported_mime_types: SUPPORTED_MIME, image_size_limit: MAX_MEDIA_BYTES, image_matrix_limit: 16777216 },
|
||||
polls: { max_options: 4, max_characters_per_option: 50, min_expiration: 300, max_expiration: 2629746 }
|
||||
},
|
||||
@@ -143,7 +151,7 @@ export async function instanceV2(env: Env): Promise<Response> {
|
||||
configuration: {
|
||||
urls: { streaming: `wss://${hostFromBaseUrl(env)}` },
|
||||
accounts: { max_featured_tags: 0 },
|
||||
statuses: { max_characters: MAX_STATUS_CHARS, max_media_attachments: MAX_MEDIA_ATTACHMENTS, characters_reserved_per_url: 23 },
|
||||
statuses: { max_characters: MAX_STATUS_CHARS, max_media_attachments: REPORTED_MEDIA_ATTACHMENTS_LIMIT, characters_reserved_per_url: 23 },
|
||||
media_attachments: { supported_mime_types: SUPPORTED_MIME, image_size_limit: MAX_MEDIA_BYTES, image_matrix_limit: 16777216 }
|
||||
},
|
||||
registrations: { enabled: false, approval_required: false, message: null },
|
||||
@@ -454,6 +462,9 @@ export async function accountStatuses(request: Request, env: Env, accountId: str
|
||||
const excludeReplies = url.searchParams.get("exclude_replies") === "true";
|
||||
const where: string[] = ["user_id = ?"];
|
||||
const binds: unknown[] = [user.id];
|
||||
const viewer = await loadStatusViewer(request, env);
|
||||
const visibilityClause = await visibleStatusWhereForOwner(env, user.id, viewer);
|
||||
if (visibilityClause) where.push(visibilityClause);
|
||||
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 ?`;
|
||||
@@ -542,11 +553,11 @@ export async function createStatus(request: Request, env: Env): Promise<Response
|
||||
const summary = bodyString(body, "spoiler_text");
|
||||
const sensitive = bodyString(body, "sensitive") === "true";
|
||||
const visibility = bodyString(body, "visibility", "public");
|
||||
if (!isStatusVisibility(visibility)) return json({ error: "invalid_visibility" }, 422);
|
||||
const inReplyTo = bodyString(body, "in_reply_to_id");
|
||||
const language = bodyString(body, "language", "en");
|
||||
|
||||
const mediaIds = bodyArray(body, "media_ids");
|
||||
if (mediaIds.length > MAX_MEDIA_ATTACHMENTS) return json({ error: "too_many_attachments" }, 422);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const statusId = id();
|
||||
@@ -619,7 +630,7 @@ export async function createStatus(request: Request, env: Env): Promise<Response
|
||||
const status = await getStatus(env, statusId);
|
||||
if (!status) throw new HttpError(500, "status_not_found");
|
||||
|
||||
if (visibility === "public" || visibility === "unlisted") {
|
||||
if (visibility === "public" || visibility === "unlisted" || visibility === "private") {
|
||||
const inboxes = new Set<string>(await gatherFollowerInboxes(env, user.id));
|
||||
for (const mention of resolvedMentions) {
|
||||
if (!mention.actorId.startsWith(baseUrl(env))) {
|
||||
@@ -627,8 +638,17 @@ export async function createStatus(request: Request, env: Env): Promise<Response
|
||||
if (cache) inboxes.add(cache.shared_inbox ?? cache.inbox);
|
||||
}
|
||||
}
|
||||
const to = visibility === "public" ? ["https://www.w3.org/ns/activitystreams#Public"] : [`${actorUrl(env, user)}/followers`];
|
||||
const cc = visibility === "public" ? [`${actorUrl(env, user)}/followers`, ...resolvedMentions.map((m) => m.actorId)] : ["https://www.w3.org/ns/activitystreams#Public", ...resolvedMentions.map((m) => m.actorId)];
|
||||
const mentionActors = resolvedMentions.map((m) => m.actorId);
|
||||
const to = visibility === "public"
|
||||
? ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
: visibility === "unlisted"
|
||||
? [`${actorUrl(env, user)}/followers`]
|
||||
: [`${actorUrl(env, user)}/followers`, ...mentionActors];
|
||||
const cc = visibility === "public"
|
||||
? [`${actorUrl(env, user)}/followers`, ...mentionActors]
|
||||
: visibility === "unlisted"
|
||||
? ["https://www.w3.org/ns/activitystreams#Public", ...mentionActors]
|
||||
: [];
|
||||
const activity = createActivity(env, user, status, { to, cc });
|
||||
await deliverToInboxes(env, user, inboxes, activity);
|
||||
} else if (visibility === "direct") {
|
||||
@@ -649,6 +669,8 @@ export async function createStatus(request: Request, env: Env): Promise<Response
|
||||
export async function getStatusEndpoint(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const status = await getStatus(env, statusId);
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
const viewer = await loadStatusViewer(request, env);
|
||||
if (!await canViewerViewStatus(env, status, viewer)) return json({ error: "Record not found" }, 404);
|
||||
const user = await getUserById(env, status.user_id);
|
||||
if (!user) return json({ error: "Record not found" }, 404);
|
||||
return json(await statusJson(env, status, user, request));
|
||||
@@ -661,7 +683,10 @@ export async function deleteStatusEndpoint(request: Request, env: Env, statusId:
|
||||
|
||||
const serialized = await statusJson(env, status, user, request);
|
||||
|
||||
const inboxes = new Set<string>(await gatherFollowerInboxes(env, user.id));
|
||||
const inboxes = new Set<string>();
|
||||
if (status.visibility !== "direct") {
|
||||
for (const inbox of await gatherFollowerInboxes(env, user.id)) inboxes.add(inbox);
|
||||
}
|
||||
const mentions = await listMentionsForStatus(env, status.id);
|
||||
for (const mention of mentions) {
|
||||
if (!mention.actor.startsWith(baseUrl(env))) {
|
||||
@@ -686,13 +711,22 @@ export async function deleteStatusEndpoint(request: Request, env: Env, statusId:
|
||||
await env.DB.prepare("DELETE FROM reblogs WHERE status_id = ?").bind(status.id).run();
|
||||
await env.DB.prepare("DELETE FROM notifications WHERE status_id = ?").bind(status.id).run();
|
||||
|
||||
await deliverToInboxes(env, user, inboxes, deleteActivity(env, user, status));
|
||||
const mentionActors = mentions.map((mention) => mention.actor);
|
||||
const deleteAudience = status.visibility === "direct"
|
||||
? { to: mentionActors, cc: [] }
|
||||
: status.visibility === "private"
|
||||
? { to: [`${actorUrl(env, user)}/followers`, ...mentionActors], cc: [] }
|
||||
: {};
|
||||
await deliverToInboxes(env, user, inboxes, deleteActivity(env, user, status, deleteAudience));
|
||||
return json(serialized);
|
||||
}
|
||||
|
||||
export async function statusContext(env: Env, statusId: string, request: Request): Promise<Response> {
|
||||
const status = await getStatus(env, statusId);
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
const viewer = await loadStatusViewer(request, env);
|
||||
if (!await canViewerViewStatus(env, status, viewer)) return json({ error: "Record not found" }, 404);
|
||||
|
||||
const ancestors: Status[] = [];
|
||||
let cursor = status.in_reply_to_id;
|
||||
while (cursor) {
|
||||
@@ -702,17 +736,19 @@ export async function statusContext(env: Env, statusId: string, request: Request
|
||||
cursor = parent.in_reply_to_id;
|
||||
}
|
||||
const descRows = await env.DB.prepare("SELECT * FROM statuses WHERE in_reply_to_id = ? ORDER BY created_at ASC LIMIT 40").bind(statusId).all<Status>();
|
||||
const serialized = await serializeStatuses(env, [...ancestors, ...descRows.results], request);
|
||||
const visibleAncestors = await filterStatusesForViewer(env, ancestors, viewer);
|
||||
const visibleDescendants = await filterStatusesForViewer(env, descRows.results, viewer);
|
||||
const serialized = await serializeStatuses(env, [...visibleAncestors, ...visibleDescendants], request);
|
||||
const byId = new Map(serialized.map((item) => [String(item.id), item]));
|
||||
return json({
|
||||
ancestors: ancestors.map((item) => byId.get(item.id)).filter(Boolean),
|
||||
descendants: descRows.results.map((item) => byId.get(item.id)).filter(Boolean)
|
||||
ancestors: visibleAncestors.map((item) => byId.get(item.id)).filter(Boolean),
|
||||
descendants: visibleDescendants.map((item) => byId.get(item.id)).filter(Boolean)
|
||||
});
|
||||
}
|
||||
|
||||
export async function favouriteStatus(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const user = await requireUser(request, env);
|
||||
const status = await getStatus(env, statusId);
|
||||
const status = await visibleStatusOrNull(env, statusId, statusViewerForUser(env, user));
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
|
||||
const actor = actorUrl(env, user);
|
||||
@@ -739,7 +775,7 @@ export async function favouriteStatus(request: Request, env: Env, statusId: stri
|
||||
|
||||
export async function unfavouriteStatus(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const user = await requireUser(request, env);
|
||||
const status = await getStatus(env, statusId);
|
||||
const status = await visibleStatusOrNull(env, statusId, statusViewerForUser(env, user));
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
|
||||
const actor = actorUrl(env, user);
|
||||
@@ -758,8 +794,9 @@ export async function unfavouriteStatus(request: Request, env: Env, statusId: st
|
||||
|
||||
export async function reblogStatus(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const user = await requireUser(request, env);
|
||||
const status = await getStatus(env, statusId);
|
||||
const status = await visibleStatusOrNull(env, statusId, statusViewerForUser(env, user));
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
if (status.visibility !== "public" && status.visibility !== "unlisted") return json({ error: "status_not_rebloggable" }, 422);
|
||||
|
||||
const actor = actorUrl(env, user);
|
||||
const existing = await findReblog(env, status.id, actor);
|
||||
@@ -785,7 +822,7 @@ export async function reblogStatus(request: Request, env: Env, statusId: string)
|
||||
|
||||
export async function unreblogStatus(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const user = await requireUser(request, env);
|
||||
const status = await getStatus(env, statusId);
|
||||
const status = await visibleStatusOrNull(env, statusId, statusViewerForUser(env, user));
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
|
||||
const actor = actorUrl(env, user);
|
||||
@@ -805,7 +842,7 @@ export async function unreblogStatus(request: Request, env: Env, statusId: strin
|
||||
|
||||
export async function bookmarkStatus(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const user = await requireUser(request, env);
|
||||
const status = await getStatus(env, statusId);
|
||||
const status = await visibleStatusOrNull(env, statusId, statusViewerForUser(env, user));
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
await addBookmark(env, user.id, status.id);
|
||||
const owner = await getUserById(env, status.user_id);
|
||||
@@ -815,7 +852,7 @@ export async function bookmarkStatus(request: Request, env: Env, statusId: strin
|
||||
|
||||
export async function unbookmarkStatus(request: Request, env: Env, statusId: string): Promise<Response> {
|
||||
const user = await requireUser(request, env);
|
||||
const status = await getStatus(env, statusId);
|
||||
const status = await visibleStatusOrNull(env, statusId, statusViewerForUser(env, user));
|
||||
if (!status) return json({ error: "Record not found" }, 404);
|
||||
await removeBookmark(env, user.id, status.id);
|
||||
const owner = await getUserById(env, status.user_id);
|
||||
@@ -847,8 +884,9 @@ export async function bookmarksList(request: Request, env: Env): Promise<Respons
|
||||
`SELECT s.* FROM statuses s INNER JOIN bookmarks b ON b.status_id = s.id
|
||||
WHERE b.user_id = ? ORDER BY b.created_at DESC LIMIT ?`
|
||||
).bind(user.id, limit).all<Status>();
|
||||
const items = await serializeStatuses(env, rows.results, request);
|
||||
return withPagination(json(items), request, rows.results.map((s) => s.id));
|
||||
const visibleRows = await filterStatusesForViewer(env, rows.results, statusViewerForUser(env, user));
|
||||
const items = await serializeStatuses(env, visibleRows, request);
|
||||
return withPagination(json(items), request, visibleRows.map((s) => s.id));
|
||||
}
|
||||
|
||||
export async function favouritesList(request: Request, env: Env): Promise<Response> {
|
||||
@@ -860,8 +898,9 @@ export async function favouritesList(request: Request, env: Env): Promise<Respon
|
||||
`SELECT s.* FROM statuses s INNER JOIN favourites f ON f.status_id = s.id
|
||||
WHERE f.actor = ? ORDER BY f.created_at DESC LIMIT ?`
|
||||
).bind(actor, limit).all<Status>();
|
||||
const items = await serializeStatuses(env, rows.results, request);
|
||||
return withPagination(json(items), request, rows.results.map((s) => s.id));
|
||||
const visibleRows = await filterStatusesForViewer(env, rows.results, statusViewerForUser(env, user));
|
||||
const items = await serializeStatuses(env, visibleRows, request);
|
||||
return withPagination(json(items), request, visibleRows.map((s) => s.id));
|
||||
}
|
||||
|
||||
export async function publicTimeline(request: Request, env: Env): Promise<Response> {
|
||||
@@ -1123,8 +1162,9 @@ export async function search(request: Request, env: Env): Promise<Response> {
|
||||
}
|
||||
|
||||
if (!type || type === "statuses") {
|
||||
const rows = await env.DB.prepare("SELECT * FROM statuses WHERE content LIKE ? ORDER BY created_at DESC LIMIT 20").bind(`%${escapeHtml(q)}%`).all<Status>();
|
||||
statuses.push(...await serializeStatuses(env, rows.results, request));
|
||||
const rows = await env.DB.prepare("SELECT * FROM statuses WHERE content LIKE ? ORDER BY created_at DESC LIMIT 100").bind(`%${escapeHtml(q)}%`).all<Status>();
|
||||
const visibleRows = await filterStatusesForViewer(env, rows.results, await loadStatusViewer(request, env));
|
||||
statuses.push(...await serializeStatuses(env, visibleRows.slice(0, 20), request));
|
||||
}
|
||||
|
||||
if (!type || type === "hashtags") {
|
||||
@@ -1311,8 +1351,9 @@ async function buildStatusSerializationContext(
|
||||
for (const user of await loadUsersByIds(env, missingUserIds)) usersById.set(user.id, user);
|
||||
}
|
||||
|
||||
const viewer = await viewerActor(request, env);
|
||||
const viewerId = await viewerUserId(request, env);
|
||||
const viewerUserForContext = await viewerUser(request, env);
|
||||
const viewer = viewerUserForContext ? actorUrl(env, viewerUserForContext) : null;
|
||||
const viewerId = viewerUserForContext?.id ?? null;
|
||||
const [mediaByStatusId, mentionsByStatusId, hashtagsByStatusId, favouriteSummary, reblogSummary, replyCountByStatusId, bookmarkedStatusIds, pinnedStatusIds] = await Promise.all([
|
||||
loadMediaByStatusIds(env, statusIds),
|
||||
loadMentionsByStatusIds(env, statusIds),
|
||||
@@ -1626,7 +1667,8 @@ async function serializeNotifications(env: Env, notifications: Notification[], r
|
||||
if (notifications.length === 0) return [];
|
||||
|
||||
const statuses = await loadStatusesByIds(env, uniqueStrings(notifications.map((notification) => notification.status_id)));
|
||||
const serializedStatuses = await serializeStatuses(env, statuses, request);
|
||||
const visibleStatuses = await filterStatusesForViewer(env, statuses, await loadStatusViewer(request, env));
|
||||
const serializedStatuses = await serializeStatuses(env, visibleStatuses, request);
|
||||
const serializedStatusById = new Map(serializedStatuses.map((item) => [String(item.id), item]));
|
||||
|
||||
const remoteActorIds = uniqueStrings(
|
||||
@@ -1708,22 +1750,68 @@ function withPagination(response: Response, request: Request, ids: string[]): Re
|
||||
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
||||
}
|
||||
|
||||
async function viewerActor(request: Request, env: Env): Promise<string | null> {
|
||||
function isStatusVisibility(value: string): value is StatusVisibility {
|
||||
return VALID_STATUS_VISIBILITIES.has(value);
|
||||
}
|
||||
|
||||
async function loadStatusViewer(request: Request, env: Env): Promise<StatusViewer> {
|
||||
return statusViewerForUser(env, await viewerUser(request, env));
|
||||
}
|
||||
|
||||
function statusViewerForUser(env: Env, user: User | null): StatusViewer {
|
||||
return {
|
||||
user,
|
||||
actor: user ? actorUrl(env, user) : null,
|
||||
followsByOwnerId: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
async function visibleStatusWhereForOwner(env: Env, ownerUserId: string, viewer: StatusViewer, column = "visibility"): Promise<string | null> {
|
||||
if (viewer.user?.id === ownerUserId) return null;
|
||||
if (await viewerFollowsOwner(env, viewer, ownerUserId)) return `${column} IN ('public', 'unlisted', 'private')`;
|
||||
return `${column} IN ('public', 'unlisted')`;
|
||||
}
|
||||
|
||||
async function visibleStatusOrNull(env: Env, statusId: string, viewer: StatusViewer): Promise<Status | null> {
|
||||
const status = await getStatus(env, statusId);
|
||||
if (!status) return null;
|
||||
return await canViewerViewStatus(env, status, viewer) ? status : null;
|
||||
}
|
||||
|
||||
async function filterStatusesForViewer(env: Env, statuses: Status[], viewer: StatusViewer): Promise<Status[]> {
|
||||
const visible: Status[] = [];
|
||||
for (const status of statuses) {
|
||||
if (await canViewerViewStatus(env, status, viewer)) visible.push(status);
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
async function canViewerViewStatus(env: Env, status: Status, viewer: StatusViewer): Promise<boolean> {
|
||||
if (status.visibility === "public" || status.visibility === "unlisted") return true;
|
||||
if (viewer.user?.id === status.user_id) return true;
|
||||
if (status.visibility === "private") return viewerFollowsOwner(env, viewer, status.user_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function viewerFollowsOwner(env: Env, viewer: StatusViewer, ownerUserId: string): Promise<boolean> {
|
||||
if (!viewer.actor) return false;
|
||||
const cached = viewer.followsByOwnerId.get(ownerUserId);
|
||||
if (cached !== undefined) return cached;
|
||||
const row = await env.DB.prepare(
|
||||
"SELECT 1 AS hit FROM follows WHERE local_user_id = ? AND follower_actor = ? AND accepted = 1 LIMIT 1"
|
||||
).bind(ownerUserId, viewer.actor).first<{ hit: number }>();
|
||||
const follows = Boolean(row?.hit);
|
||||
viewer.followsByOwnerId.set(ownerUserId, follows);
|
||||
return follows;
|
||||
}
|
||||
|
||||
async function viewerUser(request: Request, env: Env): Promise<User | null> {
|
||||
const auth = request.headers.get("authorization") ?? "";
|
||||
const token = auth.match(/^Bearer\s+(.+)$/i)?.[1];
|
||||
if (!token) return null;
|
||||
const session = await env.KV.get<Session>(`token:${token}`, "json");
|
||||
if (!session) return null;
|
||||
const user = await getUserById(env, session.userId);
|
||||
return user ? actorUrl(env, user) : null;
|
||||
}
|
||||
|
||||
async function viewerUserId(request: Request, env: Env): Promise<string | null> {
|
||||
const auth = request.headers.get("authorization") ?? "";
|
||||
const token = auth.match(/^Bearer\s+(.+)$/i)?.[1];
|
||||
if (!token) return null;
|
||||
const session = await env.KV.get<Session>(`token:${token}`, "json");
|
||||
return session?.userId ?? null;
|
||||
return getUserById(env, session.userId);
|
||||
}
|
||||
|
||||
async function loadBookmarkedStatusIds(env: Env, userId: string, statusIds: string[]): Promise<Set<string>> {
|
||||
|
||||
Reference in New Issue
Block a user