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.
This commit is contained in:
Matthias G
2026-08-03 13:13:31 +02:00
parent 05e131a0c0
commit 7e2a00842e
4 changed files with 61 additions and 6 deletions
+1 -1
View File
@@ -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.",
+1 -1
View File
@@ -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.",
+10 -4
View File
@@ -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' }
+49
View File
@@ -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<ChannelResult> {
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}` }
}