mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
The AI assistant's "more actions" need host calls today's plugin API lacks. Area c of lib/plugin-sandbox/assistant adds them: - `calendar.respond(emailId, status)`, new permission calendar:respond: answers an invitation of the user's own account through lib/calendar-rsvp.ts, like the reader's banner - `calendar.deleteEvent` now returns an undo token for a single event; `calendar.undoDelete` creates it again from the host's copy - `filters.list/create/undo` over Settings > Filters' model; forward, redirect, reject, discard and keep are refused, and so are folders that are shared or not the account's own - `email.batch/undoBatch`: up to 200 messages through the store's batch functions, restore points first, one entry in the undo history - `vacation.get` also says whether the out-of-office forwards mail - `app.describeSettings`, `app.setSettings` and `app.undoSettings`, new permission settings:change, over the allow-list in lib/settings-schema.ts, refusing locked, hidden, restricted and gated settings when the change is made Undo tokens stay in the host for 30 minutes, bound to the plugin and the account. app.getSettings now shows every setting setSettings may change. settings:write's text is reworded to what it gates (tags). plugin-host-api-app-info gains a test; its other assertions are as they were.
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
// Undo tokens for area c (email.batch, filters.create, calendar.deleteEvent,
|
|
// app.setSettings). What an undo needs stays in the host: the plugin only
|
|
// gets a random token, bound to its id and the signed-in account, good for
|
|
// 30 minutes. So a plugin can take back what it did, but can never hand the
|
|
// host a copy of something to re-create.
|
|
|
|
import { generateUUID } from '../../utils';
|
|
|
|
/** How long an undo token can be used. */
|
|
export const UNDO_TOKEN_TTL_MS = 30 * 60_000;
|
|
/** Most tokens kept per plugin; the oldest go first. */
|
|
const MAX_TOKENS_PER_PLUGIN = 50;
|
|
|
|
interface Entry {
|
|
pluginId: string;
|
|
accountId: string;
|
|
kind: string;
|
|
expires: number;
|
|
data: unknown;
|
|
}
|
|
|
|
const tokens = new Map<string, Entry>();
|
|
|
|
function prune(now: number): void {
|
|
for (const [token, entry] of tokens) if (entry.expires <= now) tokens.delete(token);
|
|
}
|
|
|
|
/** Keeps `data` for an undo of `kind` and returns its token. */
|
|
export function issueUndoToken(pluginId: string, accountId: string, kind: string, data: unknown): string {
|
|
const now = Date.now();
|
|
prune(now);
|
|
const mine = [...tokens].filter(([, e]) => e.pluginId === pluginId);
|
|
for (const [token] of mine.slice(0, Math.max(0, mine.length - MAX_TOKENS_PER_PLUGIN + 1))) tokens.delete(token);
|
|
const token = generateUUID();
|
|
tokens.set(token, { pluginId, accountId, kind, expires: now + UNDO_TOKEN_TTL_MS, data });
|
|
return token;
|
|
}
|
|
|
|
/**
|
|
* Takes the data of a token: only for the plugin that got it, in the same
|
|
* account, for the same kind, before it ran out. A token works once.
|
|
*/
|
|
export function redeemUndoToken<T>(method: string, pluginId: string, accountId: string, kind: string, token: unknown): T {
|
|
if (typeof token !== 'string' || !token) throw new Error(`${method}: undoToken must be a string`);
|
|
const entry = tokens.get(token);
|
|
prune(Date.now());
|
|
if (!entry || entry.expires <= Date.now() || entry.pluginId !== pluginId || entry.accountId !== accountId || entry.kind !== kind) {
|
|
throw new Error(`${method}: this can't be undone any more`);
|
|
}
|
|
tokens.delete(token);
|
|
return entry.data as T;
|
|
}
|
|
|
|
/** For tests. */
|
|
export function clearUndoTokens(): void {
|
|
tokens.clear();
|
|
}
|