mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
api.tasks.create and update take alerts by id: a trigger from the task's due (or start) as an ISO 8601 duration, or at an instant in UTC; display or email; at most five. An update merges by id, each alert written by its JMAP patch path: an alert given is set, one given null is removed, and alerts the plugin did not name, such as one another app wrote, stay as the server holds them; null for the whole map removes every alert. Whether the map is patched or written whole is decided on the task as the server holds it, not an older copy. An alert kept with the same trigger and action stays acknowledged, so a snooze is an update of its trigger, which then fires again. While a task uses its calendar's default alerts, a change makes them the task's own under new ids, as the task form does, an alert under a default's id is refused rather than dropped, and removing every alert turns the defaults off too. TaskView returns the alerts, each with when it was acknowledged, and useDefaultAlerts. api.tasks.acknowledgeAlert dismisses one alert (or undoes that with null) by writing only its acknowledged. A plugin's writes to one task go one after another, since Stalwart refuses a set while another is under way on the same object. The task store applies patch paths to the task the caller read, so a task the list does not hold is shown and reported whole.
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
/**
|
|
* JMAP PatchObject paths (RFC 8620 §5.3): a key such as
|
|
* "alerts/a1/acknowledged" changes that one member and leaves the rest of the
|
|
* object as the server holds it, so a change made meanwhile elsewhere (another
|
|
* device, another plugin call) is not overwritten.
|
|
*/
|
|
|
|
/** The JSON Pointer path of nested members, each segment escaped (RFC 6901). */
|
|
export function patchPath(...segments: string[]): string {
|
|
return segments.map((s) => s.replace(/~/g, '~0').replace(/\//g, '~1')).join('/');
|
|
}
|
|
|
|
function unescapeSegment(segment: string): string {
|
|
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
}
|
|
|
|
/** A shallow copy that keeps every own key, `__proto__` included, as data. */
|
|
function copyOf(object: object | null | undefined): Record<string, unknown> {
|
|
return Object.fromEntries(Object.entries(object ?? {}));
|
|
}
|
|
|
|
/** Sets `key` as an own data property, never the prototype. */
|
|
function put(object: Record<string, unknown>, key: string, value: unknown): void {
|
|
Object.defineProperty(object, key, { value, enumerable: true, configurable: true, writable: true });
|
|
}
|
|
|
|
/**
|
|
* The object as the server holds it after `patch`: plain keys replace the
|
|
* property, path keys change the member they name (null removes a map
|
|
* member). Nothing is changed in place.
|
|
*/
|
|
export function applyPatchObject<T extends object>(target: T, patch: object): T {
|
|
let result = copyOf(target);
|
|
for (const [key, value] of Object.entries(patch)) {
|
|
if (!key.includes('/')) {
|
|
put(result, key, value);
|
|
continue;
|
|
}
|
|
result = setIn(result, key.split('/').map(unescapeSegment), value);
|
|
}
|
|
return result as T;
|
|
}
|
|
|
|
function setIn(object: Record<string, unknown>, [head, ...rest]: string[], value: unknown): Record<string, unknown> {
|
|
const copy = copyOf(object);
|
|
if (rest.length === 0) {
|
|
if (value === null) delete copy[head];
|
|
else put(copy, head, value);
|
|
return copy;
|
|
}
|
|
const child = Object.hasOwn(copy, head) ? copy[head] : undefined;
|
|
put(copy, head, setIn(child && typeof child === 'object' ? (child as Record<string, unknown>) : {}, rest, value));
|
|
return copy;
|
|
}
|
|
|
|
/**
|
|
* The top-level properties `patch` changes, each whole as it is after the
|
|
* patch: a path key ("alerts/a1/acknowledged") becomes its property
|
|
* ("alerts") with every member as the patch leaves it. For whoever reads a
|
|
* patch as plain properties (plugin hooks), while the server gets the paths.
|
|
*/
|
|
export function patchedProperties<T extends object>(target: T, patch: object): Partial<T> {
|
|
const after = applyPatchObject(target, patch) as Record<string, unknown>;
|
|
const changed: Record<string, unknown> = {};
|
|
for (const key of Object.keys(patch)) {
|
|
const property = unescapeSegment(key.split('/')[0]);
|
|
put(changed, property, Object.hasOwn(after, property) ? after[property] : null);
|
|
}
|
|
return changed as Partial<T>;
|
|
}
|