Delete webhook messages too by looking up their id
A message sent through the webhook has no stored id, but it belongs to the same Slack app, so the bot token may remove it. The delete button now looks the message up in the channel by its text and time before deleting.
This commit is contained in:
+7
-3
@@ -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": {
|
||||
|
||||
+7
-3
@@ -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": {
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-12 pt-12">
|
||||
@@ -104,9 +105,9 @@ export default async function AdminSettingsPage() {
|
||||
<td className={`${cellClass} whitespace-nowrap`}>
|
||||
{run.deletedAt
|
||||
? t('messageDeleted')
|
||||
: run.messageTs
|
||||
? <MessageDeleteButton runId={run.id} />
|
||||
: run.sent ? t('messageWebhook') : '-'}
|
||||
: run.sent && canDelete
|
||||
? <MessageDeleteButton runId={run.id} hint={run.messageTs ? undefined : t('messageWebhook')} />
|
||||
: run.sent ? t('messageNoToken') : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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) {
|
||||
<AdminSubmit quiet pendingLabel={t('messageDeleting')} icon={<TbTrash className="size-4" />}>
|
||||
{t('messageDelete')}
|
||||
</AdminSubmit>
|
||||
{hint ? <span className="font-mono text-micro text-ink-3">{hint}</span> : null}
|
||||
{state.status === 'error' && state.message ? (
|
||||
<span role="alert" className={errorClass}>{messages(state.message)}</span>
|
||||
) : null}
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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<string | undefined> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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' } },
|
||||
|
||||
Reference in New Issue
Block a user