fix 编辑嘟文
This commit is contained in:
+159
-3
@@ -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(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/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),
|
||||
|
||||
Reference in New Issue
Block a user