Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .moon/tasks/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ tasks:
args: "--check"
lint:
command: "oxlint --no-error-on-unmatched-pattern"
inputs:
- "**/*"
- "/packages/config/oxlint.config.ts"
- "/packages/config/oxlint/anti-slop/**/*"
- "/packages/config/package.json"
- "/pnpm-workspace.yaml"
- "/pnpm-lock.yaml"
lint/fix:
extends: "lint"
args: "--fix"
61 changes: 36 additions & 25 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
formatLog,
getErrorContext,
getRequestId,
type LogContext,
} from "@embedly/logging";
import { Platforms } from "@embedly/platforms";
import { httpInstrumentationMiddleware } from "@hono/otel";
Expand All @@ -21,6 +22,21 @@ import { version } from "../package.json";

type ScrapeResponse = Awaited<ReturnType<(typeof Platforms)[keyof typeof Platforms]["transform"]>>;

interface ApiLogContext extends LogContext {
request_id: string;
trace_id?: string;
span_id?: string;
source: string;
platform: string;
post_id: string;
force: boolean;
cache_status: "skipped" | "miss" | "hit" | "read_error" | "stored" | "write_error";
outcome: "success" | "error";
status_code: number;
error_type?: string;
duration_ms?: number;
}

const config: ResolveConfigFn<CloudflareBindings> = (env) => {
if (!env.OTEL_ENDPOINT) throw new Error("OTEL_ENDPOINT is required.");

Expand Down Expand Up @@ -52,7 +68,7 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
const { id, platform, force } = c.req.valid("json");
const requestId = getRequestId(c.req.raw);
const spanContext = trace.getActiveSpan()?.spanContext();
const logContext: Record<string, unknown> = {
const logContext: ApiLogContext = {
request_id: requestId,
trace_id: spanContext?.traceId,
span_id: spanContext?.spanId,
Expand All @@ -73,25 +89,20 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
const cachedItem = await cache.get<ScrapeResponse>(cacheKey, "json");
if (cachedItem) {
logContext.cache_status = "hit";
return c.json(cachedItem as ScrapeResponse, 200);
return c.json(cachedItem, 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),
}),
);
}
}

// oxlint-disable-next-line import/namespace
const p = Platforms[platform as keyof typeof Platforms];
if (!p) {
if (!Object.hasOwn(Platforms, platform)) {
const problem = createProblem(EmbedlyErrors.NoMatchesFound, {
request_id: requestId,
context: logContext,
Expand All @@ -104,6 +115,8 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
});
return c.json(problem, problem.status);
}
// oxlint-disable-next-line import/namespace -- SAFETY: Object.hasOwn verified this registry key.
const p = Platforms[platform as keyof typeof Platforms];

let raw: unknown;
try {
Expand All @@ -125,6 +138,7 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()

let data: ScrapeResponse;
try {
// SAFETY: raw came from this platform's fetch implementation.
data = await p.transform(raw as any);
} catch (cause) {
const problem = createProblem(EmbedlyErrors.PlatformTransformFailed, {
Expand All @@ -145,16 +159,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
37 changes: 35 additions & 2 deletions apps/bot/src/commands/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
EmbedlyLogs,
formatDiscordError,
getErrorContext,
type LogContext,
} from "@embedly/logging";
import { Command } from "@sapphire/framework";
import {
Expand Down Expand Up @@ -32,6 +33,25 @@ const DELETE_SUCCESS_MESSAGES = [
"🧹 all tidy! embed removed as requested~",
];

interface DeleteLogContext extends LogContext {
request_id: string;
trace_id?: string;
span_id?: string;
source: "context_menu";
interaction_id: string;
channel_id: string | null;
guild_id: string;
user_id: string;
message_id?: string;
original_author_id?: string;
has_manage_permission?: boolean;
outcome: "success" | "error";
status_code: number;
error_type?: string;
reason?: string;
duration_ms?: number;
}

export class DeleteCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
Expand Down Expand Up @@ -62,7 +82,7 @@ export class DeleteCommand extends Command {

const startedAt = Date.now();
const requestId = `context_menu:${interaction.id}`;
const logContext: Record<string, unknown> = {
const logContext: DeleteLogContext = {
request_id: requestId,
source: "context_menu",
interaction_id: interaction.id,
Expand Down Expand Up @@ -196,7 +216,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 +235,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
Loading
Loading