2026-09-19 14:12:23 -04:00
|
|
|
/**
|
|
|
|
|
* [Gitea #203] Build and send an inline reply typed into a notification's
|
|
|
|
|
* text action. Lives outside sw.ts so it can be unit-tested; the SW passes in
|
2026-10-17 20:52:12 -04:00
|
|
|
* the notification account's session and the global fetch.
|
2026-09-19 14:12:23 -04:00
|
|
|
*/
|
2026-10-17 20:52:12 -04:00
|
|
|
export type SwReplySession = { accessToken: string; baseUrl: string; userId?: string };
|
|
|
|
|
|
|
|
|
|
export type SwReplyResult = 'sent' | 'failed' | 'encrypted' | 'no-session';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* [Gitea #299] The session to reply with: the notification's own account. A
|
|
|
|
|
* notification without a user id (shown before this change) may only use the
|
|
|
|
|
* session if it is the only one, so a reply never posts as another account.
|
|
|
|
|
*/
|
|
|
|
|
export const pickReplySession = (
|
|
|
|
|
sessions: SwReplySession[],
|
|
|
|
|
userId: string | undefined,
|
|
|
|
|
): SwReplySession | undefined => {
|
|
|
|
|
if (userId) return sessions.filter((x) => x.userId === userId).pop();
|
|
|
|
|
return sessions.length === 1 ? sessions[0] : undefined;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** What the "Reply not sent" notification says for each failure. */
|
|
|
|
|
export const replyFailureText = (result: SwReplyResult, text: string): string => {
|
|
|
|
|
const quoted = `"${text.slice(0, 60)}"`;
|
|
|
|
|
if (result === 'encrypted') {
|
|
|
|
|
return `This room is now encrypted, so ${quoted} wasn't sent. Open the app to reply.`;
|
|
|
|
|
}
|
|
|
|
|
if (result === 'no-session') {
|
|
|
|
|
return `You're no longer signed in to that account here, so ${quoted} wasn't sent.`;
|
|
|
|
|
}
|
|
|
|
|
return `Couldn't send ${quoted}. Open the app to retry.`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const encryptionStateUrl = (baseUrl: string, roomId: string): string =>
|
|
|
|
|
new URL(
|
|
|
|
|
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.encryption/`,
|
|
|
|
|
baseUrl,
|
|
|
|
|
).href;
|
2026-09-19 14:12:23 -04:00
|
|
|
|
|
|
|
|
export const buildReplyContent = (text: string, threadId?: string): Record<string, unknown> => {
|
|
|
|
|
const content: Record<string, unknown> = { msgtype: 'm.text', body: text, 'm.mentions': {} };
|
|
|
|
|
if (threadId) {
|
|
|
|
|
content['m.relates_to'] = {
|
|
|
|
|
rel_type: 'm.thread',
|
|
|
|
|
event_id: threadId,
|
|
|
|
|
is_falling_back: true,
|
|
|
|
|
'm.in_reply_to': { event_id: threadId },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return content;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const replySendUrl = (baseUrl: string, roomId: string, txnId: string): string =>
|
|
|
|
|
new URL(
|
|
|
|
|
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
|
|
|
|
baseUrl,
|
|
|
|
|
).href;
|
|
|
|
|
|
|
|
|
|
export async function sendNotificationReply(
|
|
|
|
|
session: SwReplySession | undefined,
|
|
|
|
|
roomId: string,
|
|
|
|
|
threadId: string | undefined,
|
|
|
|
|
text: string,
|
|
|
|
|
fetchFn: typeof fetch = fetch,
|
2026-10-17 20:52:12 -04:00
|
|
|
): Promise<SwReplyResult> {
|
|
|
|
|
if (!session?.accessToken || !session.baseUrl) return 'no-session';
|
|
|
|
|
const headers = { Authorization: `Bearer ${session.accessToken}` };
|
2026-09-19 14:12:23 -04:00
|
|
|
try {
|
2026-10-17 20:52:12 -04:00
|
|
|
// [Gitea #299] The SW can't encrypt, and the reply action was offered when
|
|
|
|
|
// the notification was shown — possibly hours ago. Re-check now: only a
|
|
|
|
|
// definite "no encryption state" (404) lets the plain reply go out.
|
|
|
|
|
const enc = await fetchFn(encryptionStateUrl(session.baseUrl, roomId), { headers });
|
|
|
|
|
if (enc.ok) return 'encrypted';
|
|
|
|
|
if (enc.status !== 404) return 'failed';
|
|
|
|
|
const txn = `swreply-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
2026-09-19 14:12:23 -04:00
|
|
|
const res = await fetchFn(replySendUrl(session.baseUrl, roomId, txn), {
|
|
|
|
|
method: 'PUT',
|
2026-10-17 20:52:12 -04:00
|
|
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
2026-09-19 14:12:23 -04:00
|
|
|
body: JSON.stringify(buildReplyContent(text, threadId)),
|
|
|
|
|
});
|
2026-10-17 20:52:12 -04:00
|
|
|
return res.ok ? 'sent' : 'failed';
|
2026-09-19 14:12:23 -04:00
|
|
|
} catch {
|
2026-10-17 20:52:12 -04:00
|
|
|
return 'failed';
|
2026-09-19 14:12:23 -04:00
|
|
|
}
|
|
|
|
|
}
|