Skip to content

Commit 3a3aa63

Browse files
authored
Merge pull request chigwell#129 from samplec0de/feat/forward-multi-and-grouped-id
feat(messages): forward batches and auto-preserve albums
2 parents a249518 + 33f3e72 commit 3a3aa63

1 file changed

Lines changed: 115 additions & 4 deletions

File tree

telegram_mcp/tools/messages.py

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,9 @@ async def list_messages(
576576
"date": msg.date,
577577
"text": sanitize_user_content(msg.message),
578578
}
579+
grouped_id = getattr(msg, "grouped_id", None)
580+
if grouped_id is not None:
581+
record["grouped_id"] = grouped_id
579582
reply_to_id = getattr(msg.reply_to, "reply_to_msg_id", None) if msg.reply_to else None
580583
if reply_to_id:
581584
record["reply_to"] = reply_to_id
@@ -639,6 +642,9 @@ async def get_message_context(
639642
"is_target": msg.id == message_id,
640643
"text": sanitize_user_content(msg.message),
641644
}
645+
grouped_id = getattr(msg, "grouped_id", None)
646+
if grouped_id is not None:
647+
record["grouped_id"] = grouped_id
642648

643649
# Check if this message is a reply and get the replied message
644650
if msg.reply_to and msg.reply_to.reply_to_msg_id:
@@ -683,19 +689,72 @@ async def get_message_context(
683689
@validate_id("from_chat_id", "to_chat_id")
684690
async def forward_message(
685691
from_chat_id: Union[int, str],
686-
message_id: int,
692+
message_id: Union[int, List[int]],
687693
to_chat_id: Union[int, str],
688694
account: str = None,
695+
expand_album: bool = True,
689696
) -> str:
690697
"""
691-
Forward a message from one chat to another.
698+
Forward a message (or several) from a source chat to a destination chat.
699+
700+
When forwarding a single int message_id, the server automatically detects
701+
Telegram albums (multi-photo/video posts sharing a `grouped_id`) and
702+
forwards the ENTIRE album as one grouped batch — so the destination
703+
receives the album intact with "Forwarded from <source>", not a single
704+
detached photo. This is the desired behavior in almost all cases.
705+
706+
Set expand_album=False to forward only the exact message you specified
707+
(useful if you really want one photo out of an album).
708+
709+
To forward a specific set of unrelated messages, pass a list of ints.
710+
Album expansion is not applied to list inputs — the list is treated as
711+
the explicit batch.
712+
713+
Args:
714+
from_chat_id: Source chat (id or @username).
715+
message_id: A single message id (int) OR a list of ids. Single ints
716+
are auto-expanded to the full album when applicable.
717+
to_chat_id: Destination chat (id or @username).
718+
account: Optional account label for multi-account mode.
719+
expand_album: If True (default) and message_id is a single int, the
720+
server expands albums automatically. No effect on list inputs.
692721
"""
693722
try:
694723
cl = get_client(account)
695724
from_entity = await resolve_entity(from_chat_id, cl)
696725
to_entity = await resolve_entity(to_chat_id, cl)
697-
await cl.forward_messages(to_entity, message_id, from_entity)
698-
return f"Message {message_id} forwarded from {from_chat_id} to {to_chat_id}."
726+
727+
ids_to_forward = message_id
728+
expanded_from_album = False
729+
if expand_album and isinstance(message_id, int):
730+
anchor = await cl.get_messages(from_entity, ids=message_id)
731+
grouped_id = getattr(anchor, "grouped_id", None) if anchor else None
732+
if grouped_id is not None:
733+
# Album ids are allocated contiguously by Telegram; a small
734+
# window around the anchor reliably captures all siblings.
735+
window = list(range(message_id - 9, message_id + 10))
736+
neighbors = await cl.get_messages(from_entity, ids=window)
737+
sibling_ids = sorted(
738+
{
739+
m.id
740+
for m in neighbors
741+
if m is not None and getattr(m, "grouped_id", None) == grouped_id
742+
}
743+
)
744+
if len(sibling_ids) > 1:
745+
ids_to_forward = sibling_ids
746+
expanded_from_album = True
747+
748+
await cl.forward_messages(to_entity, ids_to_forward, from_entity)
749+
count = len(ids_to_forward) if isinstance(ids_to_forward, list) else 1
750+
if count == 1:
751+
return f"Message {message_id} forwarded from {from_chat_id} to {to_chat_id}."
752+
if expanded_from_album:
753+
return (
754+
f"Album of {count} messages forwarded from {from_chat_id} "
755+
f"to {to_chat_id} (auto-expanded from message {message_id})."
756+
)
757+
return f"{count} messages forwarded from {from_chat_id} to {to_chat_id}."
699758
except Exception as e:
700759
return log_and_format_error(
701760
"forward_message",
@@ -706,6 +765,58 @@ async def forward_message(
706765
)
707766

708767

768+
@mcp.tool(
769+
annotations=ToolAnnotations(
770+
title="Forward Messages (batch)", openWorldHint=True, destructiveHint=True
771+
)
772+
)
773+
@with_account(readonly=False)
774+
@validate_id("from_chat_id", "to_chat_id")
775+
async def forward_messages(
776+
from_chat_id: Union[int, str],
777+
message_ids: List[int],
778+
to_chat_id: Union[int, str],
779+
account: str = None,
780+
) -> str:
781+
"""
782+
Forward a BATCH of messages from a source chat to a destination chat in
783+
a single atomic call.
784+
785+
Use this whenever you need to forward more than one message. Pass all
786+
message ids as a list (e.g. message_ids=[12345, 12346, 12347]). Calling
787+
this once with a list is strictly better than calling forward_message
788+
multiple times: it preserves Telegram album grouping (siblings sharing
789+
`grouped_id` arrive as one grouped album), is atomic, and counts as a
790+
single forward op for Telegram rate limits.
791+
792+
For exactly one message, you may use either this tool with a one-item
793+
list or `forward_message` with an int.
794+
795+
Args:
796+
from_chat_id: Source chat (id or @username).
797+
message_ids: List of message ids to forward, in any order
798+
(e.g. [12345, 12346]). Must contain at least one id.
799+
to_chat_id: Destination chat (id or @username).
800+
account: Optional account label for multi-account mode.
801+
"""
802+
try:
803+
if not message_ids:
804+
return "Error: message_ids must contain at least one id."
805+
cl = get_client(account)
806+
from_entity = await resolve_entity(from_chat_id, cl)
807+
to_entity = await resolve_entity(to_chat_id, cl)
808+
await cl.forward_messages(to_entity, list(message_ids), from_entity)
809+
return f"{len(message_ids)} messages forwarded from " f"{from_chat_id} to {to_chat_id}."
810+
except Exception as e:
811+
return log_and_format_error(
812+
"forward_messages",
813+
e,
814+
from_chat_id=from_chat_id,
815+
message_ids=message_ids,
816+
to_chat_id=to_chat_id,
817+
)
818+
819+
709820
@mcp.tool(
710821
annotations=ToolAnnotations(
711822
title="Edit Message", openWorldHint=True, destructiveHint=True, idempotentHint=True

0 commit comments

Comments
 (0)