Skip to content

feat: update memo when its source message is edited - #208

Open
nord-winter wants to merge 4 commits into
usememos:mainfrom
nord-winter:edit-mirror
Open

feat: update memo when its source message is edited#208
nord-winter wants to merge 4 commits into
usememos:mainfrom
nord-winter:edit-mirror

Conversation

@nord-winter

@nord-winter nord-winter commented Jul 30, 2026

Copy link
Copy Markdown

Closes #207

Tracks which memo a Telegram message created, keyed by message ID.
Needed to support updating a memo when the source message is later
edited. Persisted the same way as the existing user access token
map: a flat file, atomic rename on write, in-memory sync.Map cache.
Currently editing a message you sent to the bot triggers an error:
the update arrives with Message == nil (EditedMessage is set instead),
which fails the existing nil check and sends "invalid message
structure: missing required fields" back to the user.

Route EditedMessage updates to a dedicated handler before that check.
It looks up the memo the original message created and calls
UpdateMemo with the new content.

No reply is sent on success, matching the existing pattern where
Telegram already marks the message as edited client-side.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nord-winter, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b93a3b57-fb12-4574-acf6-68e8ce90cfdf

📥 Commits

Reviewing files that changed from the base of the PR and between eae2efd and 555766f.

📒 Files selected for processing (5)
  • memogram.go
  • store/message.go
  • store/message_test.go
  • store/store.go
  • store/user.go

Walkthrough

The handler detects edited Telegram messages before normal validation, validates access and message mappings, converts edited text or captions, and updates the associated memo. Newly created memos record chat and message IDs. The Store persists and reloads chat-scoped mappings, with tests covering persistence, missing mappings, and duplicate message IDs across chats. Documentation describes the editing behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: updating a memo when its source Telegram message is edited.
Description check ✅ Passed The description references the directly related issue being addressed by the pull request.
Linked Issues check ✅ Passed The changes address issue #207 by handling edited messages, updating mapped memos, ignoring invalid edits, and scoping mappings by chat and message IDs.
Out of Scope Changes check ✅ Passed The documentation, storage changes, and tests directly support the edited-message handling and chat-scoped memo mapping objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
README.md (1)

118-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Redundant wording: "originally created".

✏️ Proposed wording fix
-- Edit a sent message: Update the memo that message originally created. Edits to messages Memogram never captured (for example, messages sent before this feature was available) are ignored.
+- Edit a sent message: Update the memo that message created. Edits to messages Memogram never captured (for example, messages sent before this feature was available) are ignored.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 118, Remove the redundant wording “originally created”
from the README description of editing a sent message, while preserving the
existing meaning about updating the message’s memo and ignoring uncaptured
edits.

Source: Linters/SAST tools

memogram.go (1)

211-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate content-extraction logic between handler() and editedMessageHandler.

Both blocks pick Text/Entities, override with Caption/CaptionEntities, then run formatContent — identical logic. Extract into a shared helper, e.g. extractContent(text, caption string, textEntities, captionEntities []models.MessageEntity) string, to avoid future drift between the create and edit paths.

Also applies to: 367-375

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@memogram.go` around lines 211 - 219, Extract the duplicated text/caption
selection and formatting logic from handler() and editedMessageHandler into a
shared extractContent helper accepting text, caption, textEntities, and
captionEntities. Replace both inline blocks with calls to this helper,
preserving the existing preference for captions and applying formatContent only
when entities are present.
store/message.go (3)

68-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant file open when creating a fresh .messages file.

When the file doesn't exist, it's created via os.Create (with Close deferred to the end of the function) and then immediately reopened via os.Open a few lines later — two file handles to the same freshly-created (empty) file. Since a newly created file has nothing to scan, you can just return nil right after creating it.

♻️ Proposed simplification
 func (s *Store) loadMessageMemoMapFromFile() error {
 	dataPath := s.messageMemoMapDataPath()
 	if _, err := os.Stat(dataPath); os.IsNotExist(err) {
 		file, err := os.Create(dataPath)
 		if err != nil {
 			return err
 		}
-		defer file.Close()
+		return file.Close()
 	}
 
 	file, err := os.Open(dataPath)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/message.go` around lines 68 - 97, Update loadMessageMemoMapFromFile so
the os.IsNotExist branch returns nil immediately after successfully creating and
closing the new file, avoiding the subsequent os.Open on an empty file. Preserve
the existing error return when os.Create fails and the normal scanning flow for
files that already exist.

22-27: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Unbounded growth + synchronous full-file rewrite on every message.

Every SetMemoForMessage call re-snapshots the entire map, sorts it, and rewrites the whole .messages file — and entries are never pruned. Since Telegram only allows editing messages within a limited time window, retaining every mapping forever means both the in-memory sync.Map and the on-disk file grow indefinitely, and each write's cost scales with the total number of memos ever created by the service's lifetime.

Consider either capping/evicting old entries (e.g., LRU by insertion order or by tracking last-write time) or batching/debouncing the persistence instead of doing a full synchronous rewrite per message.

Also applies to: 33-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/message.go` around lines 22 - 27, The SetMemoForMessage persistence
flow causes unbounded memo retention and rewrites the complete memo file
synchronously on every update. Update SetMemoForMessage and
saveMessageMemoMapToFile to bound or evict stale entries and batch/debounce
persistence so each message update does not trigger an immediate full-file
rewrite, while preserving memo lookup and persistence behavior for retained
entries.

41-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unchecked error returns flagged by golangci-lint (errcheck).

Lines 41, 46, 51, 55, 75, 82 ignore returns from os.Remove/file.Close()/tmpFile.Close(). Most are benign defer/error-path cleanups, but consider at least logging them for observability (e.g., a failed os.Remove leaving stray temp files behind).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/message.go` around lines 41 - 82, Handle the ignored cleanup errors in
the temporary-file write flow and loadMessageMemoMapFromFile: wrap deferred
os.Remove and file.Close calls so their errors are checked and logged or
otherwise propagated appropriately. Preserve the existing primary error returns,
while ensuring cleanup failures—especially removal of temporary files—are
observable and satisfy errcheck.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@store/message.go`:
- Around line 14-27: Change the message memo cache from messageID-only keys to a
composite (Chat.ID, Message.ID) key across GetMemoForMessage and
SetMemoForMessage, preserving the existing persisted file format while updating
serialization/deserialization accordingly. Update memogram.go call sites and
edit handling to pass both chat and message identifiers, ensuring identical
message IDs in different Telegram chats remain independent.

---

Nitpick comments:
In `@memogram.go`:
- Around line 211-219: Extract the duplicated text/caption selection and
formatting logic from handler() and editedMessageHandler into a shared
extractContent helper accepting text, caption, textEntities, and
captionEntities. Replace both inline blocks with calls to this helper,
preserving the existing preference for captions and applying formatContent only
when entities are present.

In `@README.md`:
- Line 118: Remove the redundant wording “originally created” from the README
description of editing a sent message, while preserving the existing meaning
about updating the message’s memo and ignoring uncaptured edits.

In `@store/message.go`:
- Around line 68-97: Update loadMessageMemoMapFromFile so the os.IsNotExist
branch returns nil immediately after successfully creating and closing the new
file, avoiding the subsequent os.Open on an empty file. Preserve the existing
error return when os.Create fails and the normal scanning flow for files that
already exist.
- Around line 22-27: The SetMemoForMessage persistence flow causes unbounded
memo retention and rewrites the complete memo file synchronously on every
update. Update SetMemoForMessage and saveMessageMemoMapToFile to bound or evict
stale entries and batch/debounce persistence so each message update does not
trigger an immediate full-file rewrite, while preserving memo lookup and
persistence behavior for retained entries.
- Around line 41-82: Handle the ignored cleanup errors in the temporary-file
write flow and loadMessageMemoMapFromFile: wrap deferred os.Remove and
file.Close calls so their errors are checked and logged or otherwise propagated
appropriately. Preserve the existing primary error returns, while ensuring
cleanup failures—especially removal of temporary files—are observable and
satisfy errcheck.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1efbf941-993a-4ea5-aa16-50bd8ef36e48

📥 Commits

Reviewing files that changed from the base of the PR and between ea70bc8 and 32d3569.

📒 Files selected for processing (5)
  • README.md
  • memogram.go
  • store/message.go
  • store/message_test.go
  • store/store.go

Comment thread store/message.go Outdated
@nord-winter nord-winter changed the title Edit mirror feat: update memo when its source message is edited Jul 30, 2026
Telegram message IDs are only unique within a chat, not globally. The
mapping was keyed by message ID alone, so two chats with a colliding
message ID (very likely: IDs start low per chat, and the bot supports
multiple users via ALLOWED_USERNAMES) could silently overwrite each
other's entry, and an edit in one chat could update the wrong memo.

Key the cache and persisted file by (chat ID, message ID) instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
memogram.go (1)

356-360: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reapply the allowlist before accepting an edit.

A sender with a cached token and existing mapping can still change a memo after being removed from the allowlist (or changing usernames). The normal-message path rejects this at Lines 183-191; apply the same isUserAllowed gate here before looking up the token.

Proposed fix
 if message == nil || message.From == nil || message.Chat.ID == 0 {
     return
 }

+if !s.isUserAllowed(message.From.Username) {
+    return
+}
+
 userID := message.From.ID
 accessToken, ok := s.store.GetUserAccessToken(userID)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@memogram.go` around lines 356 - 360, Update the edit-handling path around
GetUserAccessToken to call isUserAllowed for the current sender before looking
up or accepting the cached access token. Return immediately when the sender is
no longer allowed, preserving the existing token and edit flow for allowed
users.
🧹 Nitpick comments (1)
store/message_test.go (1)

48-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reload the same-ID/different-chat case.

This verifies in-memory isolation only. Recreate the store before lookup so the test also covers the new chatID:messageID:memoName persistence format.

Proposed test adjustment
 store.SetMemoForMessage(1, 5, "memos/chat-one")
 store.SetMemoForMessage(2, 5, "memos/chat-two")

-memoName, ok := store.GetMemoForMessage(1, 5)
+reloaded := NewStore(dataPath)
+if err := reloaded.Init(); err != nil {
+    t.Fatalf("init reloaded store: %v", err)
+}
+
+memoName, ok := reloaded.GetMemoForMessage(1, 5)
 // ...
-memoName, ok = store.GetMemoForMessage(2, 5)
+memoName, ok = reloaded.GetMemoForMessage(2, 5)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/message_test.go` around lines 48 - 70, Update
TestMessageMemoMapScopedByChat to recreate or reload the Store after setting
both memos and before calling GetMemoForMessage. Reinitialize the new store with
the same dataPath, then verify both chat/message lookups still return their
respective memo names, covering persistence of the chatID:messageID:memoName
format.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@store/message.go`:
- Around line 29-32: Update SetMemoForMessage to serialize the
messageMemoCache.Store mutation together with saveMessageMemoMapToFile using the
store’s existing synchronization mechanism. Hold the lock across both operations
so concurrent calls cannot persist snapshots out of order, while preserving the
existing error logging behavior.

---

Outside diff comments:
In `@memogram.go`:
- Around line 356-360: Update the edit-handling path around GetUserAccessToken
to call isUserAllowed for the current sender before looking up or accepting the
cached access token. Return immediately when the sender is no longer allowed,
preserving the existing token and edit flow for allowed users.

---

Nitpick comments:
In `@store/message_test.go`:
- Around line 48-70: Update TestMessageMemoMapScopedByChat to recreate or reload
the Store after setting both memos and before calling GetMemoForMessage.
Reinitialize the new store with the same dataPath, then verify both chat/message
lookups still return their respective memo names, covering persistence of the
chatID:messageID:memoName format.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b38238da-20ff-42ef-a75b-f01249fd4498

📥 Commits

Reviewing files that changed from the base of the PR and between 32d3569 and eae2efd.

📒 Files selected for processing (3)
  • memogram.go
  • store/message.go
  • store/message_test.go

Comment thread store/message.go
editedMessageHandler accepted edits from anyone with a cached access
token, skipping the isUserAllowed check the normal message path
applies. A user removed from ALLOWED_USERNAMES (or renamed) could
still edit memos through a stale cached token.

Also serialize the cache-store-then-file-save sequence in
SetMemoForMessage and SetUserAccessToken with a mutex: without it,
concurrent writes can persist an older snapshot after a newer one,
losing the newer entry across a restart.

TestMessageMemoMapScopedByChat now reloads the store from disk to
cover the persisted format, not just the in-memory cache.
@boojack
boojack requested review from boojack and johnnyjoygh July 31, 2026 02:09
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.

Editing a sent message returns an error instead of being handled

1 participant