feat(plugins): expose thread_id to plugins (#99)

This commit is contained in:
banteg
2026-01-12 18:22:56 +04:00
committed by GitHub
parent 9d5fccab92
commit f638b8c32e
7 changed files with 65 additions and 20 deletions
+10
View File
@@ -183,6 +183,15 @@ Command handlers receive a `CommandContext` with:
Use `ctx.executor.run_one(...)` or `ctx.executor.run_many(...)` to reuse Takopi's Use `ctx.executor.run_one(...)` or `ctx.executor.run_many(...)` to reuse Takopi's
engine pipeline. Use `mode="capture"` to collect results and build a custom reply. engine pipeline. Use `mode="capture"` to collect results and build a custom reply.
`ctx.message` and `ctx.reply_to` are `MessageRef` objects with:
- `channel_id` (`int | str`, chat/channel id)
- `message_id` (`int | str`, message id)
- `thread_id` (`int | str | None`; set when the transport supports threads, like Telegram topics)
- `raw` (transport-specific payload, may be `None`)
Example: key per-thread state by `(ctx.message.channel_id, ctx.message.thread_id)`.
--- ---
## TransportRuntime helpers ## TransportRuntime helpers
@@ -228,6 +237,7 @@ async def on_message(...):
message_id=..., message_id=...,
text=..., text=...,
reply_to=..., reply_to=...,
thread_id=...,
) )
await handle_message( await handle_message(
exec_cfg, exec_cfg,
+5 -4
View File
@@ -19,6 +19,7 @@ from .transport import (
MessageRef, MessageRef,
RenderedMessage, RenderedMessage,
SendOptions, SendOptions,
ThreadId,
Transport, Transport,
) )
@@ -79,7 +80,7 @@ class IncomingMessage:
message_id: MessageId message_id: MessageId
text: str text: str
reply_to: MessageRef | None = None reply_to: MessageRef | None = None
thread_id: int | None = None thread_id: ThreadId | None = None
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -110,7 +111,7 @@ async def _send_or_edit_message(
reply_to: MessageRef | None = None, reply_to: MessageRef | None = None,
notify: bool = True, notify: bool = True,
replace_ref: MessageRef | None = None, replace_ref: MessageRef | None = None,
thread_id: int | None = None, thread_id: ThreadId | None = None,
) -> tuple[MessageRef | None, bool]: ) -> tuple[MessageRef | None, bool]:
msg = message msg = message
followups = message.extra.get("followups") followups = message.extra.get("followups")
@@ -248,7 +249,7 @@ async def send_initial_progress(
tracker: ProgressTracker, tracker: ProgressTracker,
resume_formatter: Callable[[ResumeToken], str] | None = None, resume_formatter: Callable[[ResumeToken], str] | None = None,
context_line: str | None = None, context_line: str | None = None,
thread_id: int | None = None, thread_id: ThreadId | None = None,
) -> ProgressMessageState: ) -> ProgressMessageState:
progress_ref: MessageRef | None = None progress_ref: MessageRef | None = None
last_rendered: RenderedMessage | None = None last_rendered: RenderedMessage | None = None
@@ -358,7 +359,7 @@ async def send_result_message(
edit_ref: MessageRef | None, edit_ref: MessageRef | None,
replace_ref: MessageRef | None = None, replace_ref: MessageRef | None = None,
delete_tag: str = "final", delete_tag: str = "final",
thread_id: int | None = None, thread_id: ThreadId | None = None,
) -> None: ) -> None:
final_msg, edited = await _send_or_edit_message( final_msg, edited = await _send_or_edit_message(
cfg.transport, cfg.transport,
+7 -6
View File
@@ -8,16 +8,17 @@ import anyio
from .context import RunContext from .context import RunContext
from .model import ResumeToken from .model import ResumeToken
from .transport import ChannelId, MessageId, ThreadId
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class ThreadJob: class ThreadJob:
chat_id: int chat_id: ChannelId
user_msg_id: int user_msg_id: MessageId
text: str text: str
resume_token: ResumeToken resume_token: ResumeToken
context: RunContext | None = None context: RunContext | None = None
thread_id: int | None = None thread_id: ThreadId | None = None
RunJob = Callable[[ThreadJob], Awaitable[None]] RunJob = Callable[[ThreadJob], Awaitable[None]]
@@ -65,12 +66,12 @@ class ThreadScheduler:
async def enqueue_resume( async def enqueue_resume(
self, self,
chat_id: int, chat_id: ChannelId,
user_msg_id: int, user_msg_id: MessageId,
text: str, text: str,
resume_token: ResumeToken, resume_token: ResumeToken,
context: RunContext | None = None, context: RunContext | None = None,
thread_id: int | None = None, thread_id: ThreadId | None = None,
) -> None: ) -> None:
await self.enqueue( await self.enqueue(
ThreadJob( ThreadJob(
+17 -1
View File
@@ -185,7 +185,11 @@ class TelegramTransport:
else None else None
) )
notify = options.notify notify = options.notify
message_thread_id = options.thread_id message_thread_id = (
cast(int | None, options.thread_id)
if options.thread_id is not None
else None
)
else: else:
reply_to_message_id = cast( reply_to_message_id = cast(
int | None, int | None,
@@ -219,10 +223,16 @@ class TelegramTransport:
notify=notify, notify=notify,
) )
message_id = sent.message_id message_id = sent.message_id
thread_id = (
sent.message_thread_id
if sent.message_thread_id is not None
else message_thread_id
)
return MessageRef( return MessageRef(
channel_id=chat_id, channel_id=chat_id,
message_id=message_id, message_id=message_id,
raw=sent, raw=sent,
thread_id=thread_id,
) )
async def edit( async def edit(
@@ -261,10 +271,16 @@ class TelegramTransport:
notify=notify, notify=notify,
) )
message_id = edited.message_id message_id = edited.message_id
thread_id = (
edited.message_thread_id
if edited.message_thread_id is not None
else ref.thread_id
)
return MessageRef( return MessageRef(
channel_id=chat_id, channel_id=chat_id,
message_id=message_id, message_id=message_id,
raw=edited, raw=edited,
thread_id=thread_id,
) )
async def delete(self, *, ref: MessageRef) -> bool: async def delete(self, *, ref: MessageRef) -> bool:
+19 -5
View File
@@ -1178,11 +1178,15 @@ class _CaptureTransport:
message: RenderedMessage, message: RenderedMessage,
options: SendOptions | None = None, options: SendOptions | None = None,
) -> MessageRef: ) -> MessageRef:
_ = options thread_id = options.thread_id if options is not None else None
ref = MessageRef(channel_id=channel_id, message_id=self._next_id) ref = MessageRef(channel_id=channel_id, message_id=self._next_id)
self._next_id += 1 self._next_id += 1
self.last_message = message self.last_message = message
return ref return MessageRef(
channel_id=ref.channel_id,
message_id=ref.message_id,
thread_id=thread_id,
)
async def edit( async def edit(
self, *, ref: MessageRef, message: RenderedMessage, wait: bool = True self, *, ref: MessageRef, message: RenderedMessage, wait: bool = True
@@ -1218,7 +1222,11 @@ class _TelegramCommandExecutor(CommandExecutor):
self._chat_id = chat_id self._chat_id = chat_id
self._user_msg_id = user_msg_id self._user_msg_id = user_msg_id
self._thread_id = thread_id self._thread_id = thread_id
self._reply_ref = MessageRef(channel_id=chat_id, message_id=user_msg_id) self._reply_ref = MessageRef(
channel_id=chat_id,
message_id=user_msg_id,
thread_id=thread_id,
)
def _apply_default_context(self, request: RunRequest) -> RunRequest: def _apply_default_context(self, request: RunRequest) -> RunRequest:
if request.context is not None: if request.context is not None:
@@ -1336,7 +1344,11 @@ async def _dispatch_command(
chat_id = msg.chat_id chat_id = msg.chat_id
user_msg_id = msg.message_id user_msg_id = msg.message_id
reply_ref = ( reply_ref = (
MessageRef(channel_id=chat_id, message_id=msg.reply_to_message_id) MessageRef(
channel_id=chat_id,
message_id=msg.reply_to_message_id,
thread_id=msg.thread_id,
)
if msg.reply_to_message_id is not None if msg.reply_to_message_id is not None
else None else None
) )
@@ -1349,7 +1361,9 @@ async def _dispatch_command(
user_msg_id=user_msg_id, user_msg_id=user_msg_id,
thread_id=msg.thread_id, thread_id=msg.thread_id,
) )
message_ref = MessageRef(channel_id=chat_id, message_id=user_msg_id) message_ref = MessageRef(
channel_id=chat_id, message_id=user_msg_id, thread_id=msg.thread_id
)
try: try:
backend = get_command(command_id, allowlist=allowlist, required=False) backend = get_command(command_id, allowlist=allowlist, required=False)
except ConfigError as exc: except ConfigError as exc:
+4 -3
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Awaitable, Callable from collections.abc import AsyncIterator, Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial from functools import partial
from typing import cast
import anyio import anyio
from anyio.abc import TaskGroup from anyio.abc import TaskGroup
@@ -417,12 +418,12 @@ async def run_main_loop(
async def run_thread_job(job: ThreadJob) -> None: async def run_thread_job(job: ThreadJob) -> None:
await run_job( await run_job(
job.chat_id, cast(int, job.chat_id),
job.user_msg_id, cast(int, job.user_msg_id),
job.text, job.text,
job.resume_token, job.resume_token,
job.context, job.context,
job.thread_id, cast(int | None, job.thread_id),
None, None,
scheduler.note_thread_known, scheduler.note_thread_known,
) )
+3 -1
View File
@@ -5,6 +5,7 @@ from typing import Any, Protocol, TypeAlias
ChannelId: TypeAlias = int | str ChannelId: TypeAlias = int | str
MessageId: TypeAlias = int | str MessageId: TypeAlias = int | str
ThreadId: TypeAlias = int | str
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -12,6 +13,7 @@ class MessageRef:
channel_id: ChannelId channel_id: ChannelId
message_id: MessageId message_id: MessageId
raw: Any | None = field(default=None, compare=False, hash=False) raw: Any | None = field(default=None, compare=False, hash=False)
thread_id: ThreadId | None = field(default=None, compare=False, hash=False)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -25,7 +27,7 @@ class SendOptions:
reply_to: MessageRef | None = None reply_to: MessageRef | None = None
notify: bool = True notify: bool = True
replace: MessageRef | None = None replace: MessageRef | None = None
thread_id: int | None = None thread_id: ThreadId | None = None
class Transport(Protocol): class Transport(Protocol):