Skip to content

fix(bot): update embeds after flag edits#94

Draft
rosethornbush wants to merge 1 commit into
mainfrom
fix/embed-flags-on-edit
Draft

fix(bot): update embeds after flag edits#94
rosethornbush wants to merge 1 commit into
mainfrom
fix/embed-flags-on-edit

Conversation

@rosethornbush

Copy link
Copy Markdown
Contributor

Summary

  • update existing Embedly messages when link modifiers change
  • keep multi-link updates aligned through request-index cache metadata
  • preserve legacy cache behavior without storing message content or URLs
  • fix spoiler-wrapped URL extraction so spoiler edits work

Behavior

Edits update in place only when URL values and order stay unchanged. URL additions, removals, replacements, and reordering are ignored. Failed refreshes keep the existing Embedly message and react to the source message with ❌.

Validation

  • apps/bot/node_modules/.bin/oxlint apps/bot/src
  • apps/bot/node_modules/.bin/oxfmt --check on changed bot files
  • direct tsdown bot build
  • parser assertions for @, !, ?@, ?!, and spoiler modifiers
  • git diff --check

Raw bot tsc remains noisy from pre-existing stale workspace declaration outputs.

@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: fea3c67

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
embedly-docs fea3c67 Commit Preview URL

Branch Preview URL
Jul 16 2026, 02:45 AM

@rosethornbush

Copy link
Copy Markdown
Contributor Author

@greptile

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds in-place Embedly message updates when a user edits link flag modifiers (@, !, ?@, ?!, ?, spoiler) on a message where URLs and their order stay unchanged. It also fixes spoiler URL extraction in utils.ts by stripping a trailing || that the URL regex was consuming, which previously caused isSpoiler to miss spoiler-wrapped URLs.

  • parseMessageURLs is moved from messageCreate.ts into handleUrls.ts and exported so messageUpdate.ts can reuse it for both old and new content parsing.
  • handleUrls gains an updateTargets map (requestIndex → botMessageId) that routes each matched URL to an existing bot message for editing rather than sending a new reply; failed targets react with ❌ and leave the original embed untouched.
  • messageCache adds an optional botMessageIndexes field (backward-compatible) to record which cache-position index corresponds to each bot message ID, enabling the alignment logic in the update path.

Confidence Score: 3/5

The update path is safe for the common case but misbehaves after a URL reorder, corrupting bot-message assignments on subsequent flag edits.

The cache stores requestIndex values anchored to message-creation time. After the user reorders URLs (correctly skipped because sameUrls is false), those indices are permanently misaligned with the new URL order. A later flag-only edit on the reordered content passes the sameUrls check against the most-recent content but queries the cache with stale indices, causing the wrong bot message to receive the wrong URL's embed.

apps/bot/src/listeners/messageUpdate.ts — the requestIndex alignment assumption after URL reorders needs a closer look before merging.

Important Files Changed

Filename Overview
apps/bot/src/listeners/messageUpdate.ts New edit-refresh path added; stale requestIndex after URL reorder can corrupt subsequent flag-only edits by mapping the wrong bot message to the wrong URL
apps/bot/src/lib/handleUrls.ts parseMessageURLs moved here from messageCreate.ts; handleUrls extended with updateTargets support; logic is correct but matchURL runs for all URLs even when only a subset are targeted
apps/bot/src/lib/messageCache.ts Adds botMessageIndexes as an optional field for backward compatibility; save/removeBotMessage/getBotMessages all updated consistently
apps/bot/src/lib/utils.ts Fixes spoiler URL extraction by stripping trailing
apps/bot/src/listeners/messageCreate.ts Simplified by importing parseMessageURLs from handleUrls.ts; no logic changes
apps/bot/src/commands/embed.ts Updated handleUrls call sites to pass options object instead of bare string; mechanical refactor, no logic change

Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/bot/src/listeners/messageUpdate.ts:38-53
**Stale `requestIndex` after URL reorder corrupts subsequent flag updates**

`requestIndex` values in the cache are assigned at message-creation time and never updated. If a user reorders URLs in a prior edit, `sameUrls` correctly returns `false` and skips that edit — but the cache entries still hold the original creation-time positions. On the *next* edit (where only a modifier changes and `sameUrls` is `true` relative to the reordered content), those stale indices are used to select bot messages, mapping the wrong bot message to the wrong URL.

Concretely: create message "url1 url2" → two bot messages (botMsg1↔index 0, botMsg2↔index 1) → user reorders to "url2 url1" (skipped, cache unchanged) → user adds `@` to url2 giving "@url2 url1" → `sameUrls` is `true` vs. the reordered content → `requestIndex=0` points to botMsg1 (originally url1) but `newUrls[0]` is url2, so botMsg1 gets overwritten with url2's embed while botMsg2 still shows url2 from creation. url1's embed disappears.

### Issue 2 of 3
apps/bot/src/listeners/messageUpdate.ts:6-13
`hasSameOptions` uses `===` on optional `force?: boolean`, so `undefined` and `false` are treated as different values. Since `parseMessageURLs` always writes `force = false` (never `undefined`), this won't trigger today, but it's a latent trap if another caller ever omits `force`. Normalizing with `??` makes the comparison explicit and safe.

```suggestion
function hasSameOptions(left: EmbedURLRequest, right: EmbedURLRequest) {
  return (
    (left.force ?? false) === (right.force ?? false) &&
    left.flags?.MediaOnly === right.flags?.MediaOnly &&
    left.flags?.SourceOnly === right.flags?.SourceOnly &&
    left.flags?.Spoiler === right.flags?.Spoiler
  );
}
```

### Issue 3 of 3
apps/bot/src/lib/handleUrls.ts:154-164
**Unnecessary `matchURL` calls for non-target URLs**

When called with `updateTargets`, `handleUrls` still invokes `matchURL` for every URL in the full `urls` array and then filters down to only the update targets. For a message with N URLs where only one flag changed, N−1 unnecessary `matchURL` evaluations happen in parallel. Consider pre-filtering `urls` to only entries whose index is in `updateTargets` before the `Promise.all`.

Reviews (1): Last reviewed commit: "fix(bot): update embeds after flag edits" | Re-trigger Greptile

Comment on lines +38 to +53
if (sameUrls) {
const updateTargets = new Map<number, string>();

for (const { id, requestIndex } of botMessages) {
if (requestIndex === undefined) continue;
const oldRequest = oldUrls[requestIndex];
const newRequest = newUrls[requestIndex];
if (!oldRequest || !newRequest) continue;
if (hasSameOptions(oldRequest, newRequest)) continue;
updateTargets.set(requestIndex, id);
}

if (updateTargets.size > 0) {
await handleUrls(newUrls, message, { updateTargets });
}
}

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 Stale requestIndex after URL reorder corrupts subsequent flag updates

requestIndex values in the cache are assigned at message-creation time and never updated. If a user reorders URLs in a prior edit, sameUrls correctly returns false and skips that edit — but the cache entries still hold the original creation-time positions. On the next edit (where only a modifier changes and sameUrls is true relative to the reordered content), those stale indices are used to select bot messages, mapping the wrong bot message to the wrong URL.

Concretely: create message "url1 url2" → two bot messages (botMsg1↔index 0, botMsg2↔index 1) → user reorders to "url2 url1" (skipped, cache unchanged) → user adds @ to url2 giving "@url2 url1" → sameUrls is true vs. the reordered content → requestIndex=0 points to botMsg1 (originally url1) but newUrls[0] is url2, so botMsg1 gets overwritten with url2's embed while botMsg2 still shows url2 from creation. url1's embed disappears.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/bot/src/listeners/messageUpdate.ts
Line: 38-53

Comment:
**Stale `requestIndex` after URL reorder corrupts subsequent flag updates**

`requestIndex` values in the cache are assigned at message-creation time and never updated. If a user reorders URLs in a prior edit, `sameUrls` correctly returns `false` and skips that edit — but the cache entries still hold the original creation-time positions. On the *next* edit (where only a modifier changes and `sameUrls` is `true` relative to the reordered content), those stale indices are used to select bot messages, mapping the wrong bot message to the wrong URL.

Concretely: create message "url1 url2" → two bot messages (botMsg1↔index 0, botMsg2↔index 1) → user reorders to "url2 url1" (skipped, cache unchanged) → user adds `@` to url2 giving "@url2 url1" → `sameUrls` is `true` vs. the reordered content → `requestIndex=0` points to botMsg1 (originally url1) but `newUrls[0]` is url2, so botMsg1 gets overwritten with url2's embed while botMsg2 still shows url2 from creation. url1's embed disappears.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +6 to +13
function hasSameOptions(left: EmbedURLRequest, right: EmbedURLRequest) {
return (
left.force === right.force &&
left.flags?.MediaOnly === right.flags?.MediaOnly &&
left.flags?.SourceOnly === right.flags?.SourceOnly &&
left.flags?.Spoiler === right.flags?.Spoiler
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 hasSameOptions uses === on optional force?: boolean, so undefined and false are treated as different values. Since parseMessageURLs always writes force = false (never undefined), this won't trigger today, but it's a latent trap if another caller ever omits force. Normalizing with ?? makes the comparison explicit and safe.

Suggested change
function hasSameOptions(left: EmbedURLRequest, right: EmbedURLRequest) {
return (
left.force === right.force &&
left.flags?.MediaOnly === right.flags?.MediaOnly &&
left.flags?.SourceOnly === right.flags?.SourceOnly &&
left.flags?.Spoiler === right.flags?.Spoiler
);
}
function hasSameOptions(left: EmbedURLRequest, right: EmbedURLRequest) {
return (
(left.force ?? false) === (right.force ?? false) &&
left.flags?.MediaOnly === right.flags?.MediaOnly &&
left.flags?.SourceOnly === right.flags?.SourceOnly &&
left.flags?.Spoiler === right.flags?.Spoiler
);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/bot/src/listeners/messageUpdate.ts
Line: 6-13

Comment:
`hasSameOptions` uses `===` on optional `force?: boolean`, so `undefined` and `false` are treated as different values. Since `parseMessageURLs` always writes `force = false` (never `undefined`), this won't trigger today, but it's a latent trap if another caller ever omits `force`. Normalizing with `??` makes the comparison explicit and safe.

```suggestion
function hasSameOptions(left: EmbedURLRequest, right: EmbedURLRequest) {
  return (
    (left.force ?? false) === (right.force ?? false) &&
    left.flags?.MediaOnly === right.flags?.MediaOnly &&
    left.flags?.SourceOnly === right.flags?.SourceOnly &&
    left.flags?.Spoiler === right.flags?.Spoiler
  );
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines 154 to +164
const matches = (
await Promise.all(
urls.map(async (request) => {
urls.map(async (request, requestIndex) => {
const match = await matchURL(request.url);
return match ? { ...request, ...match } : null;
return match ? { ...request, ...match, requestIndex } : null;
}),
)
).filter((m) => m !== null);
).filter(
(match) =>
match !== null && (!options.updateTargets || options.updateTargets.has(match.requestIndex)),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unnecessary matchURL calls for non-target URLs

When called with updateTargets, handleUrls still invokes matchURL for every URL in the full urls array and then filters down to only the update targets. For a message with N URLs where only one flag changed, N−1 unnecessary matchURL evaluations happen in parallel. Consider pre-filtering urls to only entries whose index is in updateTargets before the Promise.all.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/bot/src/lib/handleUrls.ts
Line: 154-164

Comment:
**Unnecessary `matchURL` calls for non-target URLs**

When called with `updateTargets`, `handleUrls` still invokes `matchURL` for every URL in the full `urls` array and then filters down to only the update targets. For a message with N URLs where only one flag changed, N−1 unnecessary `matchURL` evaluations happen in parallel. Consider pre-filtering `urls` to only entries whose index is in `updateTargets` before the `Promise.all`.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant