fix 编辑嘟文

This commit is contained in:
浪子
2026-05-16 10:13:22 +08:00
parent d39940cd59
commit ad6a8b0dcf
7 changed files with 197 additions and 9 deletions
+159 -3
View File
@@ -7,6 +7,7 @@ import {
followActivity,
likeActivity,
undoActivity,
updateNoteActivity,
updatePersonActivity
} from "./activitypub";
import { hashPassword, verifyPassword } from "./crypto";
@@ -136,6 +137,15 @@ type StatusCreateInput = {
pollHideTotals: boolean;
};
type StatusEditInput = {
statusText: string;
summary: string;
sensitive: boolean;
visibility: StatusVisibility;
language: string;
mediaIds: string[] | null;
};
function parseRedirectUris(value: string): string[] {
return value.split(/\s+/).map((item) => item.trim()).filter(Boolean);
}
@@ -613,7 +623,7 @@ async function publishStatus(env: Env, user: User, input: StatusCreateInput): Pr
const renderedContent = htmlContent(input.statusText, resolvedMentions.map(({ acct, url }) => ({ acct, url })), hashtags);
await env.DB.prepare(
"INSERT INTO statuses (id, user_id, content, summary, sensitive, language, visibility, in_reply_to_id, activity_id, object_id, created_at, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT INTO statuses (id, user_id, content, summary, sensitive, language, visibility, in_reply_to_id, activity_id, object_id, created_at, url, source_text, edited_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)"
)
.bind(
statusId,
@@ -627,7 +637,8 @@ async function publishStatus(env: Env, user: User, input: StatusCreateInput): Pr
activityId,
objectId,
now,
statusUrl(env, user, statusId)
statusUrl(env, user, statusId),
input.statusText
)
.run();
@@ -745,6 +756,29 @@ function parseStatusCreateInput(body: ParsedBody): StatusCreateInput {
};
}
function parseStatusEditInput(body: ParsedBody, existing: Status): StatusEditInput {
const existingText = statusSourceText(existing);
const statusText = Object.prototype.hasOwnProperty.call(body, "status")
? bodyString(body, "status").trim()
: existingText;
if (!statusText) throw new HttpError(422, "status can't be blank");
if (statusText.length > MAX_STATUS_CHARS) throw new HttpError(422, "status too long");
const visibility = bodyString(body, "visibility", existing.visibility);
if (!isStatusVisibility(visibility)) throw new HttpError(422, "invalid_visibility");
return {
statusText,
summary: bodyString(body, "spoiler_text", existing.summary),
sensitive: Object.prototype.hasOwnProperty.call(body, "sensitive")
? bodyString(body, "sensitive") === "true"
: Boolean(existing.sensitive),
visibility,
language: bodyString(body, "language", existing.language || "en"),
mediaIds: Object.prototype.hasOwnProperty.call(body, "media_ids") ? bodyArray(body, "media_ids") : null
};
}
function parsePollExpiresIn(value: string): number {
const seconds = Number(value);
if (!Number.isFinite(seconds)) throw new HttpError(422, "invalid_poll_expiration");
@@ -895,6 +929,106 @@ export async function getStatusEndpoint(request: Request, env: Env, statusId: st
return json(await statusJson(env, status, user, request));
}
export async function getStatusSource(request: Request, env: Env, statusId: string): Promise<Response> {
const user = await requireUser(request, env);
const status = await getStatus(env, statusId);
if (!status || status.user_id !== user.id) return json({ error: "Record not found" }, 404);
return json({
id: status.id,
text: statusSourceText(status),
spoiler_text: status.summary
});
}
export async function updateStatusEndpoint(request: Request, env: Env, statusId: string): Promise<Response> {
const user = await requireUser(request, env);
const status = await getStatus(env, statusId);
if (!status || status.user_id !== user.id) return json({ error: "Record not found" }, 404);
const body = await readBody(request);
const input = parseStatusEditInput(body, status);
const previousMentions = await listMentionsForStatus(env, status.id);
const mentionsAcct = extractMentions(input.statusText);
const hashtags = extractHashtags(input.statusText);
const resolvedMentions: { acct: string; actorId: string; url: string }[] = [];
for (const acct of mentionsAcct) {
const resolved = await resolveAcct(env, acct);
if (resolved) resolvedMentions.push(resolved);
}
const renderedContent = htmlContent(input.statusText, resolvedMentions.map(({ acct, url }) => ({ acct, url })), hashtags);
const editedAt = new Date().toISOString();
await env.DB.prepare(
`UPDATE statuses
SET content = ?, summary = ?, sensitive = ?, language = ?, visibility = ?, url = ?, source_text = ?, edited_at = ?
WHERE id = ? AND user_id = ?`
).bind(
renderedContent,
input.summary,
input.sensitive ? 1 : 0,
input.language,
input.visibility,
statusUrl(env, user, status.id),
input.statusText,
editedAt,
status.id,
user.id
).run();
if (input.mediaIds !== null) {
await env.DB.prepare("UPDATE media SET status_id = NULL WHERE status_id = ? AND user_id = ?").bind(status.id, user.id).run();
for (const mediaId of input.mediaIds) {
await env.DB.prepare("UPDATE media SET status_id = ? WHERE id = ? AND user_id = ?").bind(status.id, mediaId, user.id).run();
}
}
await env.DB.prepare("DELETE FROM mentions WHERE status_id = ?").bind(status.id).run();
for (const mention of resolvedMentions) {
await env.DB.prepare("INSERT OR IGNORE INTO mentions (status_id, actor, acct, url) VALUES (?, ?, ?, ?)")
.bind(status.id, mention.actorId, mention.acct, mention.url).run();
}
await env.DB.prepare("DELETE FROM hashtags WHERE status_id = ?").bind(status.id).run();
for (const tag of hashtags) {
await env.DB.prepare("INSERT OR IGNORE INTO hashtags (status_id, tag) VALUES (?, ?)").bind(status.id, tag).run();
}
const updated = await getStatus(env, status.id);
if (!updated) throw new HttpError(500, "status_not_found");
if (updated.visibility === "public" || updated.visibility === "unlisted" || updated.visibility === "private" || updated.visibility === "direct") {
const inboxes = new Set<string>();
if (updated.visibility !== "direct") {
for (const inbox of await gatherFollowerInboxes(env, user.id)) inboxes.add(inbox);
}
const remoteActors = new Set<string>([
...previousMentions.map((mention) => mention.actor),
...resolvedMentions.map((mention) => mention.actorId)
].filter((actorId) => !actorId.startsWith(baseUrl(env))));
for (const actorId of remoteActors) {
const cache = await resolveRemoteActor(env, actorId);
if (cache) inboxes.add(cache.shared_inbox ?? cache.inbox);
}
const mentionActors = resolvedMentions.map((mention) => mention.actorId);
const to = updated.visibility === "public"
? ["https://www.w3.org/ns/activitystreams#Public"]
: updated.visibility === "unlisted"
? [`${actorUrl(env, user)}/followers`]
: updated.visibility === "private"
? [`${actorUrl(env, user)}/followers`, ...mentionActors]
: mentionActors;
const cc = updated.visibility === "public"
? [`${actorUrl(env, user)}/followers`, ...mentionActors]
: updated.visibility === "unlisted"
? ["https://www.w3.org/ns/activitystreams#Public", ...mentionActors]
: [];
await deliverToInboxes(env, user, inboxes, await updateNoteActivity(env, user, updated, { to, cc }));
}
return json(await statusJson(env, updated, user, request));
}
export async function getPoll(request: Request, env: Env, pollId: string): Promise<Response> {
const poll = await env.DB.prepare("SELECT * FROM polls WHERE id = ?").bind(pollId).first<Poll>();
if (!poll) return json({ error: "Record not found" }, 404);
@@ -1868,6 +2002,28 @@ function parseCachedJson<T>(value: string): T[] {
}
}
function statusSourceText(status: Status): string {
return status.source_text || htmlToPlainText(status.content);
}
function htmlToPlainText(value: string): string {
return decodeHtmlEntities(value
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/p>\s*<p[^>]*>/gi, "\n\n")
.replace(/<[^>]*>/g, "")
.trim());
}
function decodeHtmlEntities(value: string): string {
return value
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
async function statusJson(
env: Env,
status: Status,
@@ -1898,7 +2054,7 @@ function statusRecord(env: Env, status: Status, user: User, context: StatusSeria
content: status.content,
text: status.content,
created_at: status.created_at,
edited_at: null,
edited_at: status.edited_at,
visibility: status.visibility,
language: status.language,
sensitive: Boolean(status.sensitive),