diff --git a/messages/de.json b/messages/de.json index 0f99089..2a50716 100644 --- a/messages/de.json +++ b/messages/de.json @@ -295,7 +295,10 @@ "runNotDeletable": "Dieser Lauf ging über den Webhook, dafür gibt es keine Nachrichtenkennung.", "runNoToken": "Ohne Bot-Token lässt sich in Slack nichts löschen.", "runDeleteFailed": "Slack hat das Löschen abgelehnt.", - "tokenInvalid": "Ein Bot-Token beginnt mit xoxb oder xoxp." + "tokenInvalid": "Ein Bot-Token beginnt mit xoxb oder xoxp.", + "runNotSent": "Bei diesem Lauf ging keine Nachricht raus.", + "runNoChannel": "Es ist kein Kanal hinterlegt.", + "runNotFound": "Im Kanal war keine passende Nachricht zu finden. Braucht der Bot noch das Recht channels:history?" }, "errors": { "nameRequired": "Trag einen Namen ein.", @@ -763,10 +766,11 @@ "messageDelete": "In Slack löschen", "messageDeleting": "Wird gelöscht", "messageDeleted": "Gelöscht", - "messageWebhook": "Über Webhook, nicht löschbar", + "messageWebhook": "Über Webhook, Kennung wird gesucht", "pathBot": "Versand über den Bot-Token. Nachrichten lassen sich hier wieder löschen.", "pathWebhook": "Versand über den Webhook. Nachrichten lassen sich danach nicht mehr löschen, dafür braucht es einen Bot-Token.", - "pathNone": "Noch kein Weg hinterlegt, es geht nichts raus." + "pathNone": "Noch kein Weg hinterlegt, es geht nichts raus.", + "messageNoToken": "Ohne Bot-Token nicht löschbar" } }, "filter": { diff --git a/messages/en.json b/messages/en.json index b56c918..7d70bbe 100644 --- a/messages/en.json +++ b/messages/en.json @@ -295,7 +295,10 @@ "runNotDeletable": "That run went through the webhook, so there is no message id.", "runNoToken": "Without a bot token nothing can be deleted in Slack.", "runDeleteFailed": "Slack refused the deletion.", - "tokenInvalid": "A bot token starts with xoxb or xoxp." + "tokenInvalid": "A bot token starts with xoxb or xoxp.", + "runNotSent": "That run sent no message.", + "runNoChannel": "No channel is stored.", + "runNotFound": "No matching message found in the channel. Does the bot still need channels:history?" }, "errors": { "nameRequired": "Enter a name.", @@ -763,10 +766,11 @@ "messageDelete": "Delete in Slack", "messageDeleting": "Deleting", "messageDeleted": "Deleted", - "messageWebhook": "Sent by webhook, cannot be deleted", + "messageWebhook": "Sent by webhook, id will be looked up", "pathBot": "Sending through the bot token. Messages can be deleted from here.", "pathWebhook": "Sending through the webhook. Messages cannot be deleted afterwards, that needs a bot token.", - "pathNone": "No route stored yet, nothing goes out." + "pathNone": "No route stored yet, nothing goes out.", + "messageNoToken": "Not deletable without a bot token" } }, "filter": { diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index 826bbd1..4d088e5 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -40,6 +40,7 @@ export default async function AdminSettingsPage() { const format = await getFormatter() const config = await readSlackConfig() const runs = await listReportRuns(historyCount) + const canDelete = config.hasBot return (
@@ -104,9 +105,9 @@ export default async function AdminSettingsPage() { {run.deletedAt ? t('messageDeleted') - : run.messageTs - ? - : run.sent ? t('messageWebhook') : '-'} + : run.sent && canDelete + ? + : run.sent ? t('messageNoToken') : '-'} ))} diff --git a/src/components/admin/MessageDeleteButton.tsx b/src/components/admin/MessageDeleteButton.tsx index 56d8622..2489792 100644 --- a/src/components/admin/MessageDeleteButton.tsx +++ b/src/components/admin/MessageDeleteButton.tsx @@ -10,9 +10,10 @@ import { emptyAdminFormState } from '~/lib/admin-forms' export type MessageDeleteButtonProps = { runId: string + hint?: string } -export function MessageDeleteButton({ runId }: MessageDeleteButtonProps) { +export function MessageDeleteButton({ runId, hint }: MessageDeleteButtonProps) { const t = useTranslations('admin.settings') const messages = useTranslations('admin.messages') const [state, formAction] = useActionState(deleteSlackMessage, emptyAdminFormState) @@ -27,6 +28,7 @@ export function MessageDeleteButton({ runId }: MessageDeleteButtonProps) { }> {t('messageDelete')} + {hint ? {hint} : null} {state.status === 'error' && state.message ? ( {messages(state.message)} ) : null} diff --git a/src/lib/admin-settings.ts b/src/lib/admin-settings.ts index aa7dbf1..9ae07fb 100644 --- a/src/lib/admin-settings.ts +++ b/src/lib/admin-settings.ts @@ -8,7 +8,8 @@ import { } from '~/data/repositories/settings' import { invalid, isUuid, readField, readFlag, type AdminFormState } from './admin-forms' import { runWeeklyReport } from './report-settings' -import { deleteFromSlack } from './slack' +import { deleteFromSlack, findMessageTs } from './slack' +import { reportText } from './weekly-report' import type { Viewer } from './auth-access' function looksLikeWebhook(value: string): boolean { @@ -108,18 +109,34 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData): return invalid('runUnknown') } - if (!run.channel || !run.messageTs) { - return invalid('runNotDeletable') + if (!run.sent) { + return invalid('runNotSent') } - const values = await readSettings(['slack.botToken']) + const values = await readSettings(['slack.botToken', 'slack.channel']) const token = values.get('slack.botToken')?.trim() + const channel = run.channel ?? values.get('slack.channel')?.trim() if (!token) { return invalid('runNoToken') } - const result = await deleteFromSlack(token, run.channel, run.messageTs) + if (!channel) { + return invalid('runNoChannel') + } + + const messageTs = run.messageTs ?? await findMessageTs({ + token, + channel, + text: reportText({ from: run.fromAt, to: run.toAt, total: run.total }), + around: run.createdAt, + }) + + if (!messageTs) { + return invalid('runNotFound') + } + + const result = await deleteFromSlack(token, channel, messageTs) if (!result.ok) { return invalid('runDeleteFailed') @@ -133,7 +150,7 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData): action: 'settings.reportDelete', entity: 'setting', entityId: run.id, - data: { channel: run.channel, messageTs: run.messageTs }, + data: { channel, messageTs }, }) return { status: 'ok', message: 'messageDeleted' } diff --git a/src/lib/slack.ts b/src/lib/slack.ts index 24c4a4a..6669545 100644 --- a/src/lib/slack.ts +++ b/src/lib/slack.ts @@ -78,3 +78,32 @@ export async function deleteFromSlack(token: string, channel: string, messageTs: return { ok: true, sent: { channel, messageTs } } } + +export async function findMessageTs(args: { + token: string + channel: string + text: string + around: Date + window?: number +}): Promise { + const window = args.window ?? 30 * 60 * 1000 + const oldest = (args.around.getTime() - window) / 1000 + const latest = (args.around.getTime() + window) / 1000 + + const { ok, data } = await callApi(args.token, 'conversations.history', { + channel: args.channel, + oldest: String(oldest), + latest: String(latest), + inclusive: true, + limit: 100, + }) + + if (!ok || !Array.isArray(data.messages)) { + return undefined + } + + const found = (data.messages as { text?: string, ts?: string }[]) + .find(message => typeof message.ts === 'string' && message.text === args.text) + + return found?.ts +} diff --git a/src/lib/weekly-report.ts b/src/lib/weekly-report.ts index f08da33..23e53b8 100644 --- a/src/lib/weekly-report.ts +++ b/src/lib/weekly-report.ts @@ -35,6 +35,12 @@ export type SlackMessage = { const dateFormat = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) +export function reportText(args: { from: Date, to: Date, total: number }): string { + const period = `${dateFormat.format(args.from)} bis ${dateFormat.format(args.to)}` + + return `Logbuch, ${period}: ${args.total === 1 ? '1 Eintrag' : `${args.total} Einträge`}` +} + export function windowFor(now: Date, days = reportDays): { from: Date, to: Date } { const to = new Date(now) const from = new Date(to.getTime() - days * 24 * 60 * 60 * 1000) @@ -145,7 +151,7 @@ export function buildMessage( const period = `${dateFormat.format(report.from)} bis ${dateFormat.format(report.to)}` const entries = count(report.total, 'Eintrag', 'Einträge') - const text = `Logbuch, ${period}: ${entries}` + const text = reportText(report) const blocks: unknown[] = [ { type: 'header', text: { type: 'plain_text', text: 'Logbuch' } },