mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
refactor(discord): share thread starter snapshot parsing
This commit is contained in:
@@ -635,26 +635,30 @@ function resolveDiscordMentions(text: string, message: Message): string {
|
||||
}
|
||||
|
||||
function resolveDiscordForwardedMessagesText(message: Message): string {
|
||||
const snapshots = resolveDiscordMessageSnapshots(message);
|
||||
if (snapshots.length === 0) {
|
||||
return "";
|
||||
return resolveDiscordForwardedMessagesTextFromSnapshots(resolveDiscordMessageSnapshots(message));
|
||||
}
|
||||
|
||||
function resolveDiscordMessageSnapshots(message: Message): DiscordMessageSnapshot[] {
|
||||
const rawData = (message as { rawData?: { message_snapshots?: unknown } }).rawData;
|
||||
return normalizeDiscordMessageSnapshots(
|
||||
rawData?.message_snapshots ??
|
||||
(message as { message_snapshots?: unknown }).message_snapshots ??
|
||||
(message as { messageSnapshots?: unknown }).messageSnapshots,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDiscordMessageSnapshots(snapshots: unknown): DiscordMessageSnapshot[] {
|
||||
if (!Array.isArray(snapshots)) {
|
||||
return [];
|
||||
}
|
||||
const forwardedBlocks = snapshots
|
||||
.map((snapshot) => {
|
||||
const snapshotMessage = snapshot.message;
|
||||
if (!snapshotMessage) {
|
||||
return null;
|
||||
}
|
||||
const text = resolveDiscordSnapshotMessageText(snapshotMessage);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const authorLabel = formatDiscordSnapshotAuthor(snapshotMessage.author);
|
||||
const heading = authorLabel
|
||||
? `[Forwarded message from ${authorLabel}]`
|
||||
: "[Forwarded message]";
|
||||
return `${heading}\n${text}`;
|
||||
})
|
||||
return snapshots.filter(
|
||||
(entry): entry is DiscordMessageSnapshot => Boolean(entry) && typeof entry === "object",
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDiscordForwardedMessagesTextFromSnapshots(snapshots: unknown): string {
|
||||
const forwardedBlocks = normalizeDiscordMessageSnapshots(snapshots)
|
||||
.map((snapshot) => buildDiscordForwardedMessageBlock(snapshot.message))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
if (forwardedBlocks.length === 0) {
|
||||
return "";
|
||||
@@ -662,18 +666,19 @@ function resolveDiscordForwardedMessagesText(message: Message): string {
|
||||
return forwardedBlocks.join("\n\n");
|
||||
}
|
||||
|
||||
function resolveDiscordMessageSnapshots(message: Message): DiscordMessageSnapshot[] {
|
||||
const rawData = (message as { rawData?: { message_snapshots?: unknown } }).rawData;
|
||||
const snapshots =
|
||||
rawData?.message_snapshots ??
|
||||
(message as { message_snapshots?: unknown }).message_snapshots ??
|
||||
(message as { messageSnapshots?: unknown }).messageSnapshots;
|
||||
if (!Array.isArray(snapshots)) {
|
||||
return [];
|
||||
function buildDiscordForwardedMessageBlock(
|
||||
snapshotMessage: DiscordSnapshotMessage | null | undefined,
|
||||
): string | null {
|
||||
if (!snapshotMessage) {
|
||||
return null;
|
||||
}
|
||||
return snapshots.filter(
|
||||
(entry): entry is DiscordMessageSnapshot => Boolean(entry) && typeof entry === "object",
|
||||
);
|
||||
const text = resolveDiscordSnapshotMessageText(snapshotMessage);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const authorLabel = formatDiscordSnapshotAuthor(snapshotMessage.author);
|
||||
const heading = authorLabel ? `[Forwarded message from ${authorLabel}]` : "[Forwarded message]";
|
||||
return `${heading}\n${text}`;
|
||||
}
|
||||
|
||||
function resolveDiscordSnapshotMessageText(snapshot: DiscordSnapshotMessage): string {
|
||||
|
||||
@@ -1,24 +1,88 @@
|
||||
import { ChannelType, type Client } from "@buape/carbon";
|
||||
import { StickerFormatType } from "discord-api-types/v10";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__resetDiscordThreadStarterCacheForTest,
|
||||
resolveDiscordThreadStarter,
|
||||
} from "./threading.js";
|
||||
|
||||
async function resolveStarter(
|
||||
message: Partial<Awaited<ReturnType<Client["rest"]["get"]>>>,
|
||||
resolveTimestampMs: () => number | undefined,
|
||||
) {
|
||||
const get = vi.fn().mockResolvedValue(message);
|
||||
type ThreadStarterRestMessage = {
|
||||
content?: string | null;
|
||||
embeds?: Array<{ title?: string | null; description?: string | null }>;
|
||||
message_snapshots?: Array<{
|
||||
message?: {
|
||||
content?: string | null;
|
||||
attachments?: unknown[];
|
||||
embeds?: Array<{ title?: string | null; description?: string | null }>;
|
||||
sticker_items?: unknown[];
|
||||
};
|
||||
}>;
|
||||
author?: {
|
||||
id?: string | null;
|
||||
username?: string | null;
|
||||
discriminator?: string | null;
|
||||
};
|
||||
member?: {
|
||||
roles?: string[];
|
||||
};
|
||||
timestamp?: string | null;
|
||||
};
|
||||
|
||||
function createStarterAuthor(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): NonNullable<ThreadStarterRestMessage["author"]> {
|
||||
return {
|
||||
id: "u1",
|
||||
username: "Alice",
|
||||
discriminator: "0",
|
||||
...overrides,
|
||||
} as NonNullable<ThreadStarterRestMessage["author"]>;
|
||||
}
|
||||
|
||||
function createForwardedSnapshotMessage(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
content: "",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createForwardedSnapshot(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
message: createForwardedSnapshotMessage(overrides),
|
||||
};
|
||||
}
|
||||
|
||||
function createStarterMessage(overrides: ThreadStarterRestMessage = {}): ThreadStarterRestMessage {
|
||||
return {
|
||||
content: "",
|
||||
embeds: [],
|
||||
author: createStarterAuthor(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveStarter(params: {
|
||||
message: ThreadStarterRestMessage;
|
||||
parentId?: string;
|
||||
parentType?: ChannelType;
|
||||
resolveTimestampMs?: () => number | undefined;
|
||||
}) {
|
||||
const get = vi.fn().mockResolvedValue(params.message);
|
||||
const client = { rest: { get } } as unknown as Client;
|
||||
|
||||
return resolveDiscordThreadStarter({
|
||||
const result = await resolveDiscordThreadStarter({
|
||||
channel: { id: "thread-1" },
|
||||
client,
|
||||
parentId: "parent-1",
|
||||
parentType: ChannelType.GuildText,
|
||||
resolveTimestampMs,
|
||||
parentId: params.parentId ?? "parent-1",
|
||||
parentType: params.parentType ?? ChannelType.GuildText,
|
||||
resolveTimestampMs: params.resolveTimestampMs ?? (() => undefined),
|
||||
});
|
||||
|
||||
return { get, result };
|
||||
}
|
||||
|
||||
describe("resolveDiscordThreadStarter", () => {
|
||||
@@ -27,15 +91,14 @@ describe("resolveDiscordThreadStarter", () => {
|
||||
});
|
||||
|
||||
it("falls back to joined embed title and description when content is empty", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
content: " ",
|
||||
embeds: [{ title: "Alert", description: "Details" }],
|
||||
author: { id: "u1", username: "Alice", discriminator: "0" },
|
||||
timestamp: "2026-02-24T12:00:00.000Z",
|
||||
},
|
||||
() => 123,
|
||||
);
|
||||
}),
|
||||
resolveTimestampMs: () => 123,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
text: "Alert\nDetails",
|
||||
@@ -46,14 +109,12 @@ describe("resolveDiscordThreadStarter", () => {
|
||||
});
|
||||
|
||||
it("prefers starter content over embed fallback text", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
content: "starter content",
|
||||
embeds: [{ title: "Alert", description: "Details" }],
|
||||
author: { username: "Alice", discriminator: "0" },
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("starter content should have produced a resolved starter payload");
|
||||
@@ -62,16 +123,15 @@ describe("resolveDiscordThreadStarter", () => {
|
||||
});
|
||||
|
||||
it("preserves username, tag, and role metadata for downstream visibility checks", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
content: "starter content",
|
||||
author: { id: "u1", username: "Alice", discriminator: "1234" },
|
||||
author: createStarterAuthor({ discriminator: "1234" }),
|
||||
member: {
|
||||
roles: ["role-1", "role-2"],
|
||||
},
|
||||
} as never,
|
||||
() => undefined,
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
author: "Alice#1234",
|
||||
@@ -83,24 +143,14 @@ describe("resolveDiscordThreadStarter", () => {
|
||||
});
|
||||
|
||||
it("extracts text from forwarded message snapshots when content is empty", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
content: "",
|
||||
embeds: [],
|
||||
message_snapshots: [
|
||||
{
|
||||
message: {
|
||||
content: "forwarded task content",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
author: { id: "u2", username: "Bob", discriminator: "0" },
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
message_snapshots: [createForwardedSnapshot({ content: "forwarded task content" })],
|
||||
author: createStarterAuthor({ id: "u2", username: "Bob" }),
|
||||
timestamp: "2026-04-03T07:00:00.000Z",
|
||||
} as never,
|
||||
() => 456,
|
||||
);
|
||||
}),
|
||||
resolveTimestampMs: () => 456,
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.text).toContain("forwarded task content");
|
||||
@@ -109,68 +159,101 @@ describe("resolveDiscordThreadStarter", () => {
|
||||
});
|
||||
|
||||
it("prefers content over forwarded message snapshots", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
content: "direct content",
|
||||
message_snapshots: [
|
||||
{
|
||||
message: {
|
||||
content: "forwarded content",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
author: { id: "u3", username: "Charlie", discriminator: "0" },
|
||||
} as never,
|
||||
() => undefined,
|
||||
);
|
||||
message_snapshots: [createForwardedSnapshot({ content: "forwarded content" })],
|
||||
author: createStarterAuthor({ id: "u3", username: "Charlie" }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.text).toBe("direct content");
|
||||
});
|
||||
|
||||
it("joins multiple forwarded message snapshots", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
content: "",
|
||||
embeds: [],
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
message_snapshots: [
|
||||
{
|
||||
message: {
|
||||
content: "first forwarded message",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
message: {
|
||||
content: "second forwarded message",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
},
|
||||
},
|
||||
createForwardedSnapshot({ content: "first forwarded message" }),
|
||||
createForwardedSnapshot({ content: "second forwarded message" }),
|
||||
],
|
||||
author: { id: "u5", username: "Eve", discriminator: "0" },
|
||||
} as never,
|
||||
() => undefined,
|
||||
);
|
||||
author: createStarterAuthor({ id: "u5", username: "Eve" }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.text).toContain("first forwarded message");
|
||||
expect(result!.text).toContain("second forwarded message");
|
||||
});
|
||||
|
||||
it("returns null when content, embeds, and snapshots are all empty", async () => {
|
||||
const result = await resolveStarter(
|
||||
{
|
||||
content: "",
|
||||
embeds: [],
|
||||
message_snapshots: [],
|
||||
author: { id: "u4", username: "Dave", discriminator: "0" },
|
||||
} as never,
|
||||
() => undefined,
|
||||
it("preserves forwarded attachment placeholders in thread starter context", async () => {
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
message_snapshots: [
|
||||
createForwardedSnapshot({
|
||||
attachments: [
|
||||
{
|
||||
id: "a1",
|
||||
filename: "forwarded.png",
|
||||
content_type: "image/png",
|
||||
url: "https://cdn.discordapp.com/forwarded.png",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
author: createStarterAuthor({ id: "u6", username: "Frank" }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.text).toContain("[Forwarded message]");
|
||||
expect(result!.text).toContain("<media:image> (1 image)");
|
||||
});
|
||||
|
||||
it("preserves forwarded sticker placeholders in thread starter context", async () => {
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
message_snapshots: [
|
||||
createForwardedSnapshot({
|
||||
sticker_items: [
|
||||
{
|
||||
id: "s1",
|
||||
name: "party",
|
||||
format_type: StickerFormatType.PNG,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
author: createStarterAuthor({ id: "u7", username: "Grace" }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.text).toContain("[Forwarded message]");
|
||||
expect(result!.text).toContain("<media:sticker> (1 sticker)");
|
||||
});
|
||||
|
||||
it("uses the thread id as the message channel id for forum parents", async () => {
|
||||
const { get, result } = await resolveStarter({
|
||||
message: createStarterMessage({ content: "starter content" }),
|
||||
parentId: undefined,
|
||||
parentType: ChannelType.GuildForum,
|
||||
});
|
||||
|
||||
expect(result?.text).toBe("starter content");
|
||||
expect(get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/channels/thread-1/messages/thread-1"),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null when content, embeds, and snapshots are all empty", async () => {
|
||||
const { result } = await resolveStarter({
|
||||
message: createStarterMessage({
|
||||
message_snapshots: [],
|
||||
author: createStarterAuthor({ id: "u4", username: "Dave" }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChannelType, type Client } from "@buape/carbon";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import { Routes, type APIAttachment, type APIStickerItem } from "discord-api-types/v10";
|
||||
import {
|
||||
resolveChannelModelOverride,
|
||||
type OpenClawConfig,
|
||||
@@ -14,6 +14,7 @@ import type { DiscordMessageEvent } from "./listeners.js";
|
||||
import {
|
||||
resolveDiscordChannelInfo,
|
||||
resolveDiscordEmbedText,
|
||||
resolveDiscordForwardedMessagesTextFromSnapshots,
|
||||
resolveDiscordMessageChannelId,
|
||||
} from "./message-utils.js";
|
||||
import { generateThreadTitle } from "./thread-title.js";
|
||||
@@ -42,6 +43,39 @@ type DiscordThreadParentInfo = {
|
||||
type?: ChannelType;
|
||||
};
|
||||
|
||||
type DiscordThreadStarterRestEmbed = {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
type DiscordThreadStarterRestSnapshotMessage = {
|
||||
content?: string | null;
|
||||
attachments?: APIAttachment[] | null;
|
||||
embeds?: DiscordThreadStarterRestEmbed[] | null;
|
||||
sticker_items?: APIStickerItem[] | null;
|
||||
};
|
||||
|
||||
type DiscordThreadStarterRestAuthor = {
|
||||
id?: string | null;
|
||||
username?: string | null;
|
||||
discriminator?: string | null;
|
||||
};
|
||||
|
||||
type DiscordThreadStarterRestMember = {
|
||||
nick?: string | null;
|
||||
displayName?: string | null;
|
||||
roles?: string[];
|
||||
};
|
||||
|
||||
type DiscordThreadStarterRestMessage = {
|
||||
content?: string | null;
|
||||
embeds?: DiscordThreadStarterRestEmbed[] | null;
|
||||
message_snapshots?: Array<{ message?: DiscordThreadStarterRestSnapshotMessage | null }> | null;
|
||||
member?: DiscordThreadStarterRestMember | null;
|
||||
author?: DiscordThreadStarterRestAuthor | null;
|
||||
timestamp?: string | null;
|
||||
};
|
||||
|
||||
// Cache entry with timestamp for TTL-based eviction
|
||||
type DiscordThreadStarterCacheEntry = {
|
||||
value: DiscordThreadStarter;
|
||||
@@ -98,6 +132,10 @@ function isDiscordThreadType(type: ChannelType | undefined): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isDiscordForumParentType(parentType: ChannelType | undefined): boolean {
|
||||
return parentType === ChannelType.GuildForum || parentType === ChannelType.GuildMedia;
|
||||
}
|
||||
|
||||
function resolveTrimmedDiscordMessageChannelId(params: {
|
||||
message: DiscordMessageEvent["message"];
|
||||
messageChannelId?: string;
|
||||
@@ -186,70 +224,25 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
const parentType = params.parentType;
|
||||
const isForumParent =
|
||||
parentType === ChannelType.GuildForum || parentType === ChannelType.GuildMedia;
|
||||
const messageChannelId = isForumParent ? params.channel.id : params.parentId;
|
||||
const messageChannelId = resolveDiscordThreadStarterMessageChannelId(params);
|
||||
if (!messageChannelId) {
|
||||
return null;
|
||||
}
|
||||
const starter = (await params.client.rest.get(
|
||||
Routes.channelMessage(messageChannelId, params.channel.id),
|
||||
)) as {
|
||||
content?: string | null;
|
||||
embeds?: Array<{ title?: string | null; description?: string | null }>;
|
||||
message_snapshots?: Array<{
|
||||
message?: {
|
||||
content?: string | null;
|
||||
attachments?: unknown[];
|
||||
embeds?: Array<{ title?: string | null; description?: string | null }>;
|
||||
sticker_items?: unknown[];
|
||||
};
|
||||
}>;
|
||||
member?: { nick?: string | null; displayName?: string | null };
|
||||
author?: {
|
||||
id?: string | null;
|
||||
username?: string | null;
|
||||
discriminator?: string | null;
|
||||
};
|
||||
timestamp?: string | null;
|
||||
};
|
||||
const starter = await fetchDiscordThreadStarterMessage({
|
||||
client: params.client,
|
||||
messageChannelId,
|
||||
threadId: params.channel.id,
|
||||
});
|
||||
if (!starter) {
|
||||
return null;
|
||||
}
|
||||
const content = starter.content?.trim() ?? "";
|
||||
const embedText = resolveDiscordEmbedText(starter.embeds?.[0]);
|
||||
const forwardedText = resolveStarterForwardedText(starter.message_snapshots);
|
||||
const text = content || embedText || forwardedText;
|
||||
if (!text) {
|
||||
const payload = buildDiscordThreadStarterPayload({
|
||||
starter,
|
||||
resolveTimestampMs: params.resolveTimestampMs,
|
||||
});
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
const author =
|
||||
starter.member?.nick ??
|
||||
starter.member?.displayName ??
|
||||
(starter.author
|
||||
? starter.author.discriminator && starter.author.discriminator !== "0"
|
||||
? `${starter.author.username ?? "Unknown"}#${starter.author.discriminator}`
|
||||
: (starter.author.username ?? starter.author.id ?? "Unknown")
|
||||
: "Unknown");
|
||||
const timestamp = params.resolveTimestampMs(starter.timestamp);
|
||||
const payload: DiscordThreadStarter = {
|
||||
text,
|
||||
author,
|
||||
authorId: starter.author?.id ?? undefined,
|
||||
authorName: starter.author?.username ?? undefined,
|
||||
authorTag:
|
||||
starter.author?.username && starter.author?.discriminator
|
||||
? starter.author.discriminator !== "0"
|
||||
? `${starter.author.username}#${starter.author.discriminator}`
|
||||
: starter.author.username
|
||||
: undefined,
|
||||
memberRoleIds: (() => {
|
||||
const roles = (starter.member as { roles?: string[] } | undefined)?.roles;
|
||||
return Array.isArray(roles) ? roles.map((roleId) => String(roleId)) : undefined;
|
||||
})(),
|
||||
timestamp: timestamp ?? undefined,
|
||||
};
|
||||
setCachedThreadStarter(cacheKey, payload, Date.now());
|
||||
return payload;
|
||||
} catch {
|
||||
@@ -257,6 +250,89 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDiscordThreadStarterMessageChannelId(params: {
|
||||
channel: DiscordThreadChannel;
|
||||
parentId?: string;
|
||||
parentType?: ChannelType;
|
||||
}): string | undefined {
|
||||
return isDiscordForumParentType(params.parentType) ? params.channel.id : params.parentId;
|
||||
}
|
||||
|
||||
async function fetchDiscordThreadStarterMessage(params: {
|
||||
client: Client;
|
||||
messageChannelId: string;
|
||||
threadId: string;
|
||||
}): Promise<DiscordThreadStarterRestMessage | null> {
|
||||
const starter = await params.client.rest.get(
|
||||
Routes.channelMessage(params.messageChannelId, params.threadId),
|
||||
);
|
||||
return starter ? (starter as DiscordThreadStarterRestMessage) : null;
|
||||
}
|
||||
|
||||
function buildDiscordThreadStarterPayload(params: {
|
||||
starter: DiscordThreadStarterRestMessage;
|
||||
resolveTimestampMs: (value?: string | null) => number | undefined;
|
||||
}): DiscordThreadStarter | null {
|
||||
const text = resolveDiscordThreadStarterText(params.starter);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
text,
|
||||
...resolveDiscordThreadStarterIdentity(params.starter),
|
||||
timestamp: params.resolveTimestampMs(params.starter.timestamp) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDiscordThreadStarterText(starter: DiscordThreadStarterRestMessage): string {
|
||||
const content = starter.content?.trim() ?? "";
|
||||
const embedText = resolveDiscordEmbedText(starter.embeds?.[0]);
|
||||
const forwardedText = resolveDiscordForwardedMessagesTextFromSnapshots(starter.message_snapshots);
|
||||
return content || embedText || forwardedText;
|
||||
}
|
||||
|
||||
function resolveDiscordThreadStarterIdentity(
|
||||
starter: DiscordThreadStarterRestMessage,
|
||||
): Omit<DiscordThreadStarter, "text" | "timestamp"> {
|
||||
const author = resolveDiscordThreadStarterAuthor(starter);
|
||||
return {
|
||||
author,
|
||||
authorId: starter.author?.id ?? undefined,
|
||||
authorName: starter.author?.username ?? undefined,
|
||||
authorTag: resolveDiscordThreadStarterAuthorTag(starter.author),
|
||||
memberRoleIds: resolveDiscordThreadStarterRoleIds(starter.member),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDiscordThreadStarterAuthor(starter: DiscordThreadStarterRestMessage): string {
|
||||
return (
|
||||
starter.member?.nick ??
|
||||
starter.member?.displayName ??
|
||||
resolveDiscordThreadStarterAuthorTag(starter.author) ??
|
||||
starter.author?.username ??
|
||||
starter.author?.id ??
|
||||
"Unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDiscordThreadStarterAuthorTag(
|
||||
author: DiscordThreadStarterRestAuthor | null | undefined,
|
||||
): string | undefined {
|
||||
if (!author?.username || !author.discriminator) {
|
||||
return undefined;
|
||||
}
|
||||
if (author.discriminator !== "0") {
|
||||
return `${author.username}#${author.discriminator}`;
|
||||
}
|
||||
return author.username;
|
||||
}
|
||||
|
||||
function resolveDiscordThreadStarterRoleIds(
|
||||
member: DiscordThreadStarterRestMember | null | undefined,
|
||||
): string[] | undefined {
|
||||
return Array.isArray(member?.roles) ? member.roles.map((roleId) => String(roleId)) : undefined;
|
||||
}
|
||||
|
||||
export function resolveDiscordReplyTarget(opts: {
|
||||
replyToMode: ReplyToMode;
|
||||
replyToId?: string;
|
||||
|
||||
Reference in New Issue
Block a user