alpha
Login
or
Join now
hotsocket.fyi
/
niri
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
add cdn links to discord turns
author
nekomimi.pet
date
2 months ago
(May 20, 2026, 6:57 PM -0400)
commit
924fb521
924fb52118e3868e22289410eca8fd5b88ff8fab
parent
a92b0436
a92b04365e6c51cff2aa1ddecd955978768b8566
+104
-4
4 changed files
Expand all
Collapse all
Unified
Split
src
discord
gateway.ts
parse.ts
state.ts
triggers
discord.ts
+10
src/discord/gateway.ts
View file
Reviewed
···
67
67
id: u.id,
68
68
bot: u.bot,
69
69
})),
70
70
+
attachments: message.attachments.map((a) => ({
71
71
+
id: a.id,
72
72
+
url: a.url,
73
73
+
proxy_url: a.proxyURL,
74
74
+
filename: a.name,
75
75
+
content_type: a.contentType ?? null,
76
76
+
width: a.width ?? null,
77
77
+
height: a.height ?? null,
78
78
+
size: a.size,
79
79
+
})),
70
80
},
71
81
channel: {
72
82
id: message.channelId,
+67
src/discord/parse.ts
View file
Reviewed
···
132
132
return new Set(parseChannelIds(input))
133
133
}
134
134
135
135
+
export type DiscordImageAttachment = {
136
136
+
url: string
137
137
+
filename: string | null
138
138
+
contentType: string | null
139
139
+
}
140
140
+
141
141
+
const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|webp|bmp|heic|heif|tiff?|avif)$/i
142
142
+
143
143
+
/**
144
144
+
* Determines whether a Discord attachment object is an image.
145
145
+
*
146
146
+
* @param attachment - Attachment object from a Discord message.
147
147
+
* @returns `true` when the attachment is an image by content-type or filename.
148
148
+
*/
149
149
+
function isImageAttachment(attachment: DiscordObject): boolean {
150
150
+
const contentType = asString(attachment.content_type)
151
151
+
if (contentType && contentType.toLowerCase().startsWith("image/")) return true
152
152
+
const name = asString(attachment.filename) ?? asString(attachment.url)
153
153
+
if (!name) return false
154
154
+
const pathPart = name.split("?")[0] ?? name
155
155
+
return IMAGE_EXTENSION_RE.test(pathPart)
156
156
+
}
157
157
+
158
158
+
/**
159
159
+
* Extracts image attachment CDN links from a Discord message object.
160
160
+
*
161
161
+
* Accepts either a raw event payload (with a `message` wrapper) or a message
162
162
+
* object directly. Returns the Discord CDN url for each image attachment so the
163
163
+
* agent can download it and run its own image tool on it.
164
164
+
*
165
165
+
* @param payload - Raw event payload or message object.
166
166
+
* @returns Array of image attachments with CDN urls.
167
167
+
*/
168
168
+
export function extractImageAttachments(payload: unknown): DiscordImageAttachment[] {
169
169
+
const root = asObject(payload)
170
170
+
if (!root) return []
171
171
+
const message = asObject(root.message) ?? root
172
172
+
const attachments = Array.isArray(message.attachments) ? message.attachments : []
173
173
+
const out: DiscordImageAttachment[] = []
174
174
+
for (const entry of attachments) {
175
175
+
const attachment = asObject(entry)
176
176
+
if (!attachment || !isImageAttachment(attachment)) continue
177
177
+
const url = asString(attachment.url) ?? asString(attachment.proxy_url)
178
178
+
if (!url) continue
179
179
+
out.push({
180
180
+
url,
181
181
+
filename: asString(attachment.filename),
182
182
+
contentType: asString(attachment.content_type),
183
183
+
})
184
184
+
}
185
185
+
return out
186
186
+
}
187
187
+
188
188
+
/**
189
189
+
* Extracts image attachment CDN links from a stored raw JSON payload.
190
190
+
*
191
191
+
* @param rawJson - Stored raw JSON for a Discord message.
192
192
+
* @returns Array of image attachments, empty on parse failure.
193
193
+
*/
194
194
+
export function extractImageAttachmentsFromRawJson(rawJson: string): DiscordImageAttachment[] {
195
195
+
try {
196
196
+
return extractImageAttachments(JSON.parse(rawJson))
197
197
+
} catch {
198
198
+
return []
199
199
+
}
200
200
+
}
201
201
+
135
202
/**
136
203
* Parses a raw Discord gateway/webhook payload into a structured message record.
137
204
*
+15
-2
src/discord/state.ts
View file
Reviewed
···
11
11
import {
12
12
asNumber,
13
13
asString,
14
14
+
extractImageAttachmentsFromRawJson,
14
15
parseChannelIds,
15
16
parseChannelRecord,
16
17
parseMessageRecord,
···
116
117
const parsed = new Date(value)
117
118
if (Number.isNaN(parsed.getTime())) return value
118
119
return parsed.toISOString().replace("T", " ").replace(".000Z", "Z")
120
120
+
}
121
121
+
122
122
+
function formatBatchImages(rawJson: string): string {
123
123
+
const images = extractImageAttachmentsFromRawJson(rawJson)
124
124
+
if (images.length === 0) return ""
125
125
+
return ` [images: ${images.map((img) => img.url).join(" ")}]`
119
126
}
120
127
121
128
function formatReplyContext(reply: DiscordReplyContext | undefined): string {
···
349
356
350
357
const uniqueChannels = new Set(recentMessages.map((row) => row.channel_id))
351
358
const replyContextByMessageId = buildReplyTargetContextMap([...recentMessages, ...pendingPreview])
359
359
+
const hasImages = [...recentMessages, ...pendingPreview].some(
360
360
+
(row) => extractImageAttachmentsFromRawJson(row.raw_json).length > 0,
361
361
+
)
352
362
353
363
const lines: string[] = [
354
364
`[discord batch] ${from} -> ${nowIso}`,
···
356
366
`auto_seen_timeout=${autoSeenMinutes}m auto_demoted=${autoDemotedCount}`,
357
367
`channel_flag_repairs=${repairedMessageCount}`,
358
368
"channel messages are context, not direct requests. replying is optional; use judgment.",
369
369
+
...(hasImages
370
370
+
? ["image attachments appear as [images: <discord cdn urls>]; download with shell, then inspect with image_tool if useful."]
371
371
+
: []),
359
372
"",
360
373
"recent messages:",
361
374
]
···
365
378
const author = row.author_username ? `@${row.author_username}` : "@unknown"
366
379
const ts = formatBatchTimestamp(row.created_at)
367
380
const replyTo = replyContextByMessageId.get(row.message_id)
368
368
-
lines.push(`- [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}`)
381
381
+
lines.push(`- [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}${formatBatchImages(row.raw_json)}`)
369
382
}
370
383
371
384
if (truncated) {
···
382
395
const author = row.author_username ? `@${row.author_username}` : "@unknown"
383
396
const ts = formatBatchTimestamp(row.created_at)
384
397
const replyTo = replyContextByMessageId.get(row.message_id)
385
385
-
lines.push(`- ${row.item_id} [${row.bucket}] [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}`)
398
398
+
lines.push(`- ${row.item_id} [${row.bucket}] [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}${formatBatchImages(row.raw_json)}`)
386
399
}
387
400
}
388
401
+12
-2
src/triggers/discord.ts
View file
Reviewed
···
1
1
import type { UserMessage } from "../types"
2
2
import { getDb } from "../db"
3
3
-
import { asObject, asString } from "../discord/parse"
3
3
+
import { asObject, asString, extractImageAttachments } from "../discord/parse"
4
4
5
5
function asIsoTimestamp(value: unknown, fallback: string): string {
6
6
if (typeof value !== "string" && typeof value !== "number") return fallback
···
58
58
return `reply_to: ${author} msg/${reply.message_id}: ${JSON.stringify(reply.content)}`
59
59
}
60
60
61
61
+
function formatImageBlock(images: ReturnType<typeof extractImageAttachments>): string {
62
62
+
if (images.length === 0) return ""
63
63
+
const lines = images.map((img) => {
64
64
+
const meta = [img.filename, img.contentType].filter(Boolean).join(", ")
65
65
+
return `- ${img.url}${meta ? ` (${meta})` : ""}`
66
66
+
})
67
67
+
return `\n\nimages (discord cdn links — download with shell, then inspect with image_tool if useful):\n${lines.join("\n")}`
68
68
+
}
69
69
+
61
70
export function fromDiscord(body: unknown): UserMessage {
62
71
const b = body as Record<string, unknown>
63
72
const message = (typeof b.message === "object" && b.message
···
98
107
? "This is a direct message. Reply if it needs a response."
99
108
: "This is a server channel message, not a DM. You may choose not to reply; only respond if useful."
100
109
const replyLine = replyContextLine(message, b)
110
110
+
const imageBlock = formatImageBlock(extractImageAttachments(body))
101
111
102
112
return {
103
113
source: "discord",
104
114
triggeredAt,
105
105
-
content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}`,
115
115
+
content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}${imageBlock}`,
106
116
raw: body,
107
117
}
108
118
}