feat(frontend): watches, webhooks and alerts panels read humanly, with chat picker, url filter, test send and delivery log
This commit is contained in:
1 parent
6a4fd6ad47
commit
ea3a7c0a61
19 files changed
+3699
-458
No files matched your search
@@ -0,0 +1,242 @@
|
||||
import type {
|
||||
AlertEvent,
|
||||
AlertItem,
|
||||
ChatRef,
|
||||
Delivery,
|
||||
PersonRef,
|
||||
StoredParams,
|
||||
WatchKind,
|
||||
} from "#lib/api/watches.js";
|
||||
import { formatTime } from "#lib/format/datetime.js";
|
||||
import { locale, t } from "#lib/i18n/locale.svelte.js";
|
||||
|
||||
export const KIND_ICONS: Record<WatchKind, string> = {
|
||||
keyword: "search",
|
||||
message_deleted: "delete",
|
||||
message_edited: "edit",
|
||||
new_message: "message",
|
||||
peer_online: "user-online",
|
||||
};
|
||||
|
||||
export const EVENT_ICONS: Record<AlertEvent, string> = {
|
||||
delete: "delete",
|
||||
edit: "edit",
|
||||
message: "message",
|
||||
online: "user-online",
|
||||
transcript: "transcribe",
|
||||
};
|
||||
|
||||
export const WATCH_KINDS: WatchKind[] = [
|
||||
"keyword",
|
||||
"new_message",
|
||||
"message_deleted",
|
||||
"message_edited",
|
||||
"peer_online",
|
||||
];
|
||||
|
||||
export interface Picked {
|
||||
hasAvatar: boolean;
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Described {
|
||||
chat: ChatRef | null;
|
||||
kind: WatchKind;
|
||||
params: StoredParams;
|
||||
person: PersonRef | null;
|
||||
}
|
||||
|
||||
const MINUTE = 60_000;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
const HOURS_AS_DAYS = 48;
|
||||
const WHITESPACE = /\s+/g;
|
||||
|
||||
export function chatTitle(chat: ChatRef | null, id?: number): string {
|
||||
if (chat?.title) {
|
||||
return chat.title;
|
||||
}
|
||||
return t("watches.unknownChat", { id: chat?.id ?? id ?? "" });
|
||||
}
|
||||
|
||||
export function personName(person: PersonRef | null, id?: number): string {
|
||||
if (person?.name) {
|
||||
return person.name;
|
||||
}
|
||||
return t("watches.unknownPerson", { id: person?.id ?? id ?? "" });
|
||||
}
|
||||
|
||||
export function patternOf(params: StoredParams): string {
|
||||
return params.pattern ?? params.keyword ?? "";
|
||||
}
|
||||
|
||||
function what(params: StoredParams): string {
|
||||
const key = params.regex ? "watches.describe.regex" : "watches.describe.word";
|
||||
return t(key, { pattern: patternOf(params) });
|
||||
}
|
||||
|
||||
function describeKeyword(watch: Described): string {
|
||||
const { params } = watch;
|
||||
const values = {
|
||||
chat: chatTitle(watch.chat, params.chat_id),
|
||||
person: personName(watch.person, params.sender_id),
|
||||
what: what(params),
|
||||
};
|
||||
if (params.chat_id !== undefined && params.sender_id !== undefined) {
|
||||
return t("watches.describe.keywordFromIn", values);
|
||||
}
|
||||
if (params.chat_id !== undefined) {
|
||||
return t("watches.describe.keywordIn", values);
|
||||
}
|
||||
if (params.sender_id !== undefined) {
|
||||
return t("watches.describe.keywordFrom", values);
|
||||
}
|
||||
return t("watches.describe.keyword", values);
|
||||
}
|
||||
|
||||
function describeNew(watch: Described): string {
|
||||
const { params } = watch;
|
||||
const values = {
|
||||
chat: chatTitle(watch.chat, params.chat_id),
|
||||
person: personName(watch.person, params.sender_id),
|
||||
};
|
||||
if (params.chat_id !== undefined && params.sender_id !== undefined) {
|
||||
return t("watches.describe.newFromIn", values);
|
||||
}
|
||||
if (params.chat_id !== undefined) {
|
||||
return t("watches.describe.newIn", values);
|
||||
}
|
||||
return t("watches.describe.newFrom", values);
|
||||
}
|
||||
|
||||
export function describeWatch(watch: Described): string {
|
||||
const { params } = watch;
|
||||
const chat = { chat: chatTitle(watch.chat, params.chat_id) };
|
||||
switch (watch.kind) {
|
||||
case "keyword":
|
||||
return describeKeyword(watch);
|
||||
case "new_message":
|
||||
return describeNew(watch);
|
||||
case "message_deleted":
|
||||
return params.chat_id === undefined
|
||||
? t("watches.describe.deletedDm")
|
||||
: t("watches.describe.deletedIn", chat);
|
||||
case "message_edited":
|
||||
return params.chat_id === undefined
|
||||
? t("watches.describe.editedDm")
|
||||
: t("watches.describe.editedIn", chat);
|
||||
default:
|
||||
return t("watches.describe.online", {
|
||||
person: personName(watch.person, params.peer_id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function unitFormat(unit: "minute" | "hour" | "day", value: number): string {
|
||||
return new Intl.NumberFormat(locale.tag, {
|
||||
style: "unit",
|
||||
unit,
|
||||
unitDisplay: "short",
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
export function awayFor(since: string, until: string): string {
|
||||
const gap = Math.max(
|
||||
0,
|
||||
new Date(until).getTime() - new Date(since).getTime()
|
||||
);
|
||||
if (gap < HOUR) {
|
||||
return unitFormat("minute", Math.max(1, Math.round(gap / MINUTE)));
|
||||
}
|
||||
if (gap < HOURS_AS_DAYS * HOUR) {
|
||||
return unitFormat("hour", Math.round(gap / HOUR));
|
||||
}
|
||||
return unitFormat("day", Math.round(gap / DAY));
|
||||
}
|
||||
|
||||
export function alertTitle(alert: AlertItem): string {
|
||||
if (alert.chat) {
|
||||
return chatTitle(alert.chat);
|
||||
}
|
||||
return personName(alert.peer);
|
||||
}
|
||||
|
||||
export function alertLabel(alert: AlertItem): string {
|
||||
if (alert.watch.name) {
|
||||
return alert.watch.name;
|
||||
}
|
||||
if (alert.watch.kind === "keyword") {
|
||||
return what(alert.watch.params);
|
||||
}
|
||||
return t(`alerts.event.${alert.event}`);
|
||||
}
|
||||
|
||||
export function alertPreview(alert: AlertItem): string {
|
||||
if (alert.event === "online") {
|
||||
return alert.away_since
|
||||
? t("alerts.onlineAfter", { away: awayFor(alert.away_since, alert.ts) })
|
||||
: "";
|
||||
}
|
||||
const { message } = alert;
|
||||
const text = message?.text?.replace(WHITESPACE, " ") || t("alerts.noText");
|
||||
const sender = message?.sender;
|
||||
if (
|
||||
!sender ||
|
||||
sender.id === alert.chat?.id ||
|
||||
alert.chat?.kind === "private"
|
||||
) {
|
||||
return text;
|
||||
}
|
||||
return `${personName(sender)}: ${text}`;
|
||||
}
|
||||
|
||||
export function alertLine(alert: AlertItem): string {
|
||||
const preview = alertPreview(alert);
|
||||
return preview ? `${alertLabel(alert)}: ${preview}` : alertLabel(alert);
|
||||
}
|
||||
|
||||
export function alertAvatar(alert: AlertItem): {
|
||||
hasAvatar: boolean;
|
||||
id: number;
|
||||
kind: "chat" | "peer";
|
||||
name: string;
|
||||
} {
|
||||
if (alert.chat) {
|
||||
return {
|
||||
hasAvatar: alert.chat.has_avatar,
|
||||
id: alert.chat.id,
|
||||
kind: alert.chat.id > 0 ? "peer" : "chat",
|
||||
name: chatTitle(alert.chat),
|
||||
};
|
||||
}
|
||||
return {
|
||||
hasAvatar: alert.peer?.has_avatar ?? false,
|
||||
id: alert.peer?.id ?? 0,
|
||||
kind: "peer",
|
||||
name: personName(alert.peer),
|
||||
};
|
||||
}
|
||||
|
||||
function deliveryReason(delivery: Delivery): string {
|
||||
return delivery.response_code === null
|
||||
? (delivery.last_error ?? "")
|
||||
: String(delivery.response_code);
|
||||
}
|
||||
|
||||
export function deliveryStatus(delivery: Delivery): string {
|
||||
const waiting =
|
||||
delivery.status === "sending" ||
|
||||
(delivery.status === "pending" && delivery.attempts === 0);
|
||||
if (waiting) {
|
||||
return t("webhooks.status.sending");
|
||||
}
|
||||
const head =
|
||||
delivery.status === "pending"
|
||||
? t("webhooks.status.pending", {
|
||||
time: formatTime(delivery.next_attempt_at),
|
||||
})
|
||||
: t(`webhooks.status.${delivery.status}`);
|
||||
const reason = deliveryReason(delivery);
|
||||
return reason ? `${head} · ${reason}` : head;
|
||||
}
|
||||
Reference in new issue
Block a user