Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5d39822
fix(bot): bind cache port to localhost
rosethornbush Aug 7, 2026
57d715c
fix(bot): isolate URL match failures
rosethornbush Aug 7, 2026
f74e983
fix(bot): tolerate cache bookkeeping errors
rosethornbush Aug 7, 2026
95a82ca
fix(api): make KV cache best-effort
rosethornbush Aug 7, 2026
58b1237
fix(bot): harden embed deletion cleanup
rosethornbush Aug 7, 2026
79089f1
fix(bot): scope spoilers to URL matches
rosethornbush Aug 7, 2026
6aa7c92
fix(platforms): tolerate incomplete context
rosethornbush Aug 7, 2026
f5ecdab
chore(bot): remove unused dotenv
rosethornbush Aug 7, 2026
c35a65d
fix(bot): always clean deleted message cache
rosethornbush Aug 7, 2026
12cd0fc
Merge remote-tracking branch 'origin/main' into fix/repo-cleanup
rosethornbush Aug 15, 2026
04fba22
fix(bot): preserve failed deletion tracking
rosethornbush Aug 15, 2026
4a13608
chore(config): add anti-slop rules
rosethornbush Aug 15, 2026
cb93137
refactor(logging): tighten log context types
rosethornbush Aug 15, 2026
fd41c01
refactor(api): validate scrape boundaries
rosethornbush Aug 15, 2026
978c2b6
chore(lint): explain rule disables
rosethornbush Aug 15, 2026
a4d0b16
refactor(bot): tighten runtime boundaries
rosethornbush Aug 15, 2026
63aa6ca
refactor(platforms): type tiktok payloads
rosethornbush Aug 15, 2026
c818d0b
refactor(platforms): type reddit payloads
rosethornbush Aug 15, 2026
5e17a14
refactor(platforms): move payload types
rosethornbush Aug 15, 2026
83eadd3
fix(platforms): type disabled adapters
rosethornbush Aug 15, 2026
9d2eac5
refactor(platforms): tighten social payloads
rosethornbush Aug 15, 2026
eeee372
refactor(platforms): remove legacy instagram
rosethornbush Aug 15, 2026
bc12621
refactor(platforms): type threads payloads
rosethornbush Aug 15, 2026
106b6df
fix(config): invalidate shared lint cache
rosethornbush Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 14 additions & 20 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,13 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
return c.json(cachedItem as ScrapeResponse, 200);
}
} catch (cause) {
const problem = createProblem(EmbedlyErrors.CacheReadFailed, {
request_id: requestId,
context: { ...logContext, ...getErrorContext(cause) },
});
Object.assign(logContext, getErrorContext(cause), {
outcome: "error",
status_code: problem.status,
error_type: problem.type,
});
return c.json(problem, problem.status);
logContext.cache_status = "read_error";
console.warn(
formatLog("warn", EmbedlyErrors.CacheReadFailed, {
...logContext,
...getErrorContext(cause),
}),
);
}
}

Expand Down Expand Up @@ -145,16 +142,13 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
});
logContext.cache_status = "stored";
} catch (cause) {
const problem = createProblem(EmbedlyErrors.CacheWriteFailed, {
request_id: requestId,
context: { ...logContext, ...getErrorContext(cause) },
});
Object.assign(logContext, getErrorContext(cause), {
outcome: "error",
status_code: problem.status,
error_type: problem.type,
});
return c.json(problem, problem.status);
logContext.cache_status = "write_error";
console.warn(
formatLog("warn", EmbedlyErrors.CacheWriteFailed, {
...logContext,
...getErrorContext(cause),
}),
);
}

return c.json(data, 200);
Expand Down
2 changes: 1 addition & 1 deletion apps/bot/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
timeout: 1s
retries: 30
ports:
- "6379:6379"
- "127.0.0.1:6379:6379"

bot:
container_name: embedly-bot
Expand Down
1 change: 0 additions & 1 deletion apps/bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"devDependencies": {
"@embedly/config": "workspace:*",
"@types/node": "catalog:",
"dotenv": "^17.4.2",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"tsdown": "^0.22.0",
Expand Down
15 changes: 14 additions & 1 deletion apps/bot/src/commands/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ export class DeleteCommand extends Command {

try {
await msg.delete();
await this.container.messageCache.removeBotMessage(msg.id);
} catch (error) {
const problem = createProblem(EmbedlyErrors.DeleteFailed, {
request_id: requestId,
Expand All @@ -216,6 +215,20 @@ export class DeleteCommand extends Command {
return;
}

try {
await this.container.messageCache.removeBotMessage(msg.id);
} catch (error) {
botErrors.add(1, {
...metricContext,
error_type: EmbedlyErrors.MessageCacheFailed.type,
});
log("warn", EmbedlyErrors.MessageCacheFailed, {
...logContext,
error_type: EmbedlyErrors.MessageCacheFailed.type,
...getErrorContext(error),
});
}

await interaction.editReply(
DELETE_SUCCESS_MESSAGES[~~(DELETE_SUCCESS_MESSAGES.length * Math.random())],
);
Expand Down
93 changes: 71 additions & 22 deletions apps/bot/src/lib/handleUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,51 @@ export async function handleUrls(

if (interaction) await interaction.deferReply();

let matchFailures = 0;
const matchContext = msg
? {
message_id: msg.id,
channel_id: msg.channelId,
guild_id: msg.guildId ?? "dm",
user_id: msg.author.id,
}
: {
interaction_id: interaction!.id,
channel_id: interaction!.channelId,
guild_id: interaction!.guildId ?? "dm",
user_id: interaction!.user.id,
};
const matches = (
await Promise.all(
urls.map(async (request) => {
const match = await matchURL(request.url);
return match ? { ...request, ...match } : null;
urls.map(async (request, index) => {
try {
const match = await matchURL(request.url);
return match ? { ...request, ...match } : null;
} catch (error) {
matchFailures++;
const requestId = msg
? `message:${msg.id}:match:${index}`
: `${embedSource}:${interaction!.id}:match:${index}`;
container.logger.warn(
formatLog("warn", EmbedlyErrors.ApiUnexpectedResponse, {
request_id: requestId,
source: embedSource,
...matchContext,
...getErrorContext(error),
}),
);
if (msg) await reactToFailure();
return null;
}
}),
)
).filter((m) => m !== null);

if (interaction && matches.length === 0) {
const requestId = `${embedSource}:${interaction.id}`;
const problem = createProblem(EmbedlyErrors.NoMatchesFound, {
const error =
matchFailures > 0 ? EmbedlyErrors.ApiUnexpectedResponse : EmbedlyErrors.NoMatchesFound;
const problem = createProblem(error, {
request_id: requestId,
context: {
request_id: requestId,
Expand All @@ -125,7 +158,7 @@ export async function handleUrls(
user_id: interaction.user.id,
},
});
container.logger.warn(formatLog("warn", EmbedlyErrors.NoMatchesFound, problem.context));
container.logger.warn(formatLog("warn", error, problem.context));
await interaction.editReply({
content: formatDiscordError(problem),
});
Expand Down Expand Up @@ -295,8 +328,9 @@ export async function handleUrls(
},
} as const;

let botMessage: Message;
try {
const botMessage = await span(
botMessage = await span(
"discord.send",
{
...spanAttributes,
Expand All @@ -315,22 +349,6 @@ export async function handleUrls(
return message;
},
);
logContext.bot_message_id = botMessage.id;
if (msg) {
await span(
"message_cache.save",
{
...spanAttributes,
"discord.source_message_id": msg.id,
"discord.bot_message_id": botMessage.id,
},
async () => {
await container.messageCache.save(msg.id, botMessage.id, msg.author.id);
},
);
}
botEmbedsCreated.add(1, metricContext);
sentEmbed = true;
} catch (error) {
recordError(requestSpan, error);
const problem = createProblem(EmbedlyErrors.DiscordSendFailed, {
Expand All @@ -348,6 +366,37 @@ export async function handleUrls(
return;
}
await interaction!.editReply(formatDiscordError(problem));
return;
}

logContext.bot_message_id = botMessage.id;
botEmbedsCreated.add(1, metricContext);
sentEmbed = true;

if (msg) {
try {
await span(
"message_cache.save",
{
...spanAttributes,
"discord.source_message_id": msg.id,
"discord.bot_message_id": botMessage.id,
},
async () => {
await container.messageCache.save(msg.id, botMessage.id, msg.author.id);
},
);
} catch (error) {
botErrors.add(1, {
...metricContext,
error_type: EmbedlyErrors.MessageCacheFailed.type,
});
log("warn", EmbedlyErrors.MessageCacheFailed, {
...logContext,
error_type: EmbedlyErrors.MessageCacheFailed.type,
...getErrorContext(error),
});
}
}
} finally {
Object.assign(logContext, getActiveTraceContext(), {
Expand Down
12 changes: 0 additions & 12 deletions apps/bot/src/lib/messageCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,6 @@ export class MessageCache {
return sourceMessage?.botMessageIds ?? [];
}

public async deleteSourceMessage(sourceMessageId: string) {
const botMessageIds = await this.getBotMessageIds(sourceMessageId);
const keys = [
this.getSourceMessageKey(sourceMessageId),
...botMessageIds.map((id) => this.getBotMessageAuthorKey(id)),
...botMessageIds.map((id) => this.getBotMessageSourceKey(id)),
];

await this.client.del(keys);
return botMessageIds;
}

public async close() {
await this.client.close();
}
Expand Down
8 changes: 0 additions & 8 deletions apps/bot/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ export function extractURLs(content: string): URLMatch[] {
});
}

export function isSpoiler(url: string, content: string): boolean {
return content.split("||").some((part, ind) => ind % 2 === 1 && part.includes(url));
}

export function isEscaped(url: string, content: string): boolean {
return content.includes(`<${url}>`);
}

export function truncate(text: string, maxLength: number) {
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
}
4 changes: 2 additions & 2 deletions apps/bot/src/listeners/messageCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MessageFlags, type Message } from "discord.js";

import type { EmbedFlags } from "../lib/builder";
import { handleUrls, type EmbedURLRequest } from "../lib/handleUrls";
import { extractURLs, isSpoiler } from "../lib/utils";
import { extractURLs } from "../lib/utils";

function parseMessageURLs(content: string) {
const urls: EmbedURLRequest[] = [];
Expand All @@ -21,7 +21,7 @@ function parseMessageURLs(content: string) {
}

const flags: Partial<EmbedFlags> = {
Spoiler: isSpoiler(match.url, content),
Spoiler: content.slice(0, match.index).split("||").length % 2 === 0,
};
let force = false;

Expand Down
63 changes: 52 additions & 11 deletions apps/bot/src/listeners/messageDelete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EmbedlyErrors, EmbedlyLogs, formatLog, getErrorContext } from "@embedly/logging";
import { Events, Listener } from "@sapphire/framework";
import type { Message, PartialMessage } from "discord.js";
import { DiscordAPIError, RESTJSONErrorCodes, type Message, type PartialMessage } from "discord.js";

export class MessageDeleteListener extends Listener<typeof Events.MessageDelete> {
public constructor(context: Listener.LoaderContext, options: Listener.Options) {
Expand All @@ -11,17 +11,51 @@ export class MessageDeleteListener extends Listener<typeof Events.MessageDelete>
}

public async run(message: Message | PartialMessage) {
const botMessageIds = await this.container.messageCache.deleteSourceMessage(message.id);
const requestId = `message:${message.id}`;
let botMessageIds: string[];
try {
botMessageIds = await this.container.messageCache.getBotMessageIds(message.id);
} catch (error) {
this.container.logger.warn(
formatLog("warn", EmbedlyErrors.MessageCacheFailed, {
request_id: requestId,
message_id: message.id,
...getErrorContext(error),
}),
);
return;
}

if (botMessageIds.length === 0) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let deletedCount = 0;
for (const botMessageId of botMessageIds) {
try {
const botMessage = await message.channel.messages.fetch(botMessageId);
await botMessage.delete();
deletedCount++;
} catch (error) {
if (error instanceof DiscordAPIError && error.code === RESTJSONErrorCodes.UnknownMessage) {
deletedCount++;
} else {
this.container.logger.warn(
formatLog("warn", EmbedlyErrors.DeleteFailed, {
request_id: requestId,
message_id: message.id,
bot_message_id: botMessageId,
...getErrorContext(error),
}),
);
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Retained deletions are never retried

When Discord returns a transient network, rate-limit, or server error after the source message has been deleted, this branch retains the generated-message mapping but schedules no retry. The mapping eventually expires, leaving the generated embed in Discord with no way to locate and delete it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/bot/src/listeners/messageDelete.ts
Line: 49

Comment:
**Retained deletions are never retried**

When Discord returns a transient network, rate-limit, or server error after the source message has been deleted, this branch retains the generated-message mapping but schedules no retry. The mapping eventually expires, leaving the generated embed in Discord with no way to locate and delete it.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional best-effort behavior. This listener makes one Discord delete attempt, logs failures, and retains the mapping until TTL. We do not want a retry queue or repeated Discord API calls for a failed delete; Discord availability is outside this bot’s cleanup guarantee. Leaving this as-is.

}
}

try {
await this.container.messageCache.removeBotMessage(botMessageId);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
} catch (error) {
this.container.logger.warn(
formatLog("warn", EmbedlyErrors.DeleteFailed, {
request_id: `message:${message.id}`,
formatLog("warn", EmbedlyErrors.MessageCacheFailed, {
request_id: requestId,
message_id: message.id,
bot_message_id: botMessageId,
...getErrorContext(error),
Expand All @@ -30,12 +64,19 @@ export class MessageDeleteListener extends Listener<typeof Events.MessageDelete>
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

this.container.logger.info(
formatLog("info", EmbedlyLogs.AutoDeleteSucceeded, {
request_id: `message:${message.id}`,
message_id: message.id,
bot_message_count: botMessageIds.length,
}),
);
const failedCount = botMessageIds.length - deletedCount;
const context = {
request_id: requestId,
message_id: message.id,
bot_message_count: botMessageIds.length,
deleted_count: deletedCount,
failed_count: failedCount,
};
if (failedCount > 0) {
this.container.logger.warn(formatLog("warn", EmbedlyErrors.DeleteFailed, context));
return;
}

this.container.logger.info(formatLog("info", EmbedlyLogs.AutoDeleteSucceeded, context));
}
}
6 changes: 1 addition & 5 deletions packages/platforms/src/platforms/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789

function parseMedia(raw: Record<string, any>): NormalizedPost["media"] {
if (raw.carousel_media) {
return raw.carousel_media.map((media: any) => ({
url: media.image_versions2.candidates[0].url,
type: "photo",
description: media.accessibility_caption,
}));
return raw.carousel_media.flatMap(parseMedia);
}
if (raw.video_versions) {
return [
Expand Down
Loading
Loading