From 7e2a00842ee3786776d56315098d9ddd5b3e8907 Mon Sep 17 00:00:00 2001 From: Matthias G Date: Mon, 3 Aug 2026 13:13:31 +0200 Subject: [PATCH] Turn a channel name into an id before touching a message conversations.history and chat.delete only take ids, so a stored name like logbuch answered channel_not_found. The name is resolved through conversations.list first, and the hint asks for the id. --- messages/de.json | 2 +- messages/en.json | 2 +- src/lib/admin-settings.ts | 14 +++++++---- src/lib/slack.ts | 49 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/messages/de.json b/messages/de.json index 9869d41..e512b12 100644 --- a/messages/de.json +++ b/messages/de.json @@ -757,7 +757,7 @@ "includeInternal": "Slack ist intern, deshalb standardmäßig an. Aus heißt nur Kunden- und öffentliche Beiträge.", "sendNow": "Schickt den Stand der letzten sieben Tage sofort in den Kanal.", "botToken": "Aus der Slack-App unter OAuth & Permissions, beginnt mit xoxb. Mit Bot-Token lassen sich Nachrichten später wieder löschen.", - "channel": "Name oder Kennung des Kanals, etwa logbuch oder C01234567." + "channel": "Am besten die Kanal-ID, etwa C01234567. In Slack: Kanal öffnen, auf den Namen klicken, unten steht die Kanal-ID. Ein Name wie logbuch geht auch, dafür braucht der Bot zusätzlich channels:read." }, "errors": { "webhookInvalid": "Das sieht nicht nach einer Slack-Webhook-Adresse aus.", diff --git a/messages/en.json b/messages/en.json index 70e5592..6bd2460 100644 --- a/messages/en.json +++ b/messages/en.json @@ -757,7 +757,7 @@ "includeInternal": "Slack is internal, so this is on by default. Off means customer and public entries only.", "sendNow": "Sends the last seven days into the channel right away.", "botToken": "From the Slack app under OAuth & Permissions, starts with xoxb. A bot token lets messages be deleted later.", - "channel": "Channel name or id, for example logbuch or C01234567." + "channel": "Prefer the channel id, for example C01234567. In Slack open the channel, click its name, the id sits at the bottom. A name like logbuch works too, but then the bot also needs channels:read." }, "errors": { "webhookInvalid": "That does not look like a Slack webhook address.", diff --git a/src/lib/admin-settings.ts b/src/lib/admin-settings.ts index e8c58b5..1d47088 100644 --- a/src/lib/admin-settings.ts +++ b/src/lib/admin-settings.ts @@ -8,7 +8,7 @@ import { } from '~/data/repositories/settings' import { invalid, isUuid, readField, readFlag, type AdminFormState } from './admin-forms' import { runWeeklyReport } from './report-settings' -import { deleteFromSlack, findMessageTs } from './slack' +import { deleteFromSlack, findMessageTs, resolveChannelId } from './slack' import { reportText } from './weekly-report' import type { Viewer } from './auth-access' @@ -132,12 +132,18 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData): return invalid('runNoChannel') } + const resolved = await resolveChannelId(token, channel) + + if (!resolved.ok) { + return invalid('slackSaid', { detail: resolved.detail }) + } + let messageTs = run.messageTs if (!messageTs) { const lookup = await findMessageTs({ token, - channel, + channel: resolved.id, text: reportText({ from: run.fromAt, to: run.toAt, total: run.total }), around: run.createdAt, }) @@ -149,7 +155,7 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData): messageTs = lookup.messageTs } - const result = await deleteFromSlack(token, channel, messageTs) + const result = await deleteFromSlack(token, resolved.id, messageTs) if (!result.ok) { return invalid('slackSaid', { detail: result.detail }) @@ -163,7 +169,7 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData): action: 'settings.reportDelete', entity: 'setting', entityId: run.id, - data: { channel, messageTs }, + data: { channel: resolved.id, messageTs }, }) return { status: 'ok', message: 'messageDeleted' } diff --git a/src/lib/slack.ts b/src/lib/slack.ts index 6cb810c..e18efc7 100644 --- a/src/lib/slack.ts +++ b/src/lib/slack.ts @@ -121,3 +121,52 @@ export async function findMessageTs(args: { return { ok: false, detail: `no_match_in_${messages.length}_messages` } } + +export type ChannelResult = + | { ok: true, id: string } + | { ok: false, detail: string } + +const channelIdPattern = /^[CGD][A-Z0-9]{6,}$/u + +export function looksLikeChannelId(value: string): boolean { + return channelIdPattern.test(value) +} + +export async function resolveChannelId(token: string, channel: string): Promise { + const wanted = channel.trim().replace(/^#/u, '') + + if (looksLikeChannelId(wanted)) { + return { ok: true, id: wanted } + } + + let cursor: string | undefined + + for (let page = 0; page < 5; page += 1) { + const { ok, data } = await callApi(token, 'conversations.list', { + types: 'public_channel,private_channel', + exclude_archived: true, + limit: 200, + ...(cursor ? { cursor } : {}), + }) + + if (!ok) { + return { ok: false, detail: String(data.error ?? 'unknown_error') } + } + + const channels = Array.isArray(data.channels) ? data.channels as { id?: string, name?: string }[] : [] + const found = channels.find(entry => entry.name === wanted) + + if (found?.id) { + return { ok: true, id: found.id } + } + + const meta = data.response_metadata as { next_cursor?: string } | undefined + cursor = meta?.next_cursor + + if (!cursor) { + break + } + } + + return { ok: false, detail: `channel_name_not_found:${wanted}` } +}