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
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
@@ -228,6 +237,7 @@ async def on_message(...):
message_id=...,
text=...,
reply_to=...,
thread_id=...,
)
await handle_message(
exec_cfg,
+5 -4
View File
@@ -19,6 +19,7 @@ from .transport import (
MessageRef,
RenderedMessage,
SendOptions,
ThreadId,
Transport,
)
@@ -79,7 +80,7 @@ class IncomingMessage:
message_id: MessageId
text: str
reply_to: MessageRef | None = None
thread_id: int | None = None
thread_id: ThreadId | None = None
@dataclass(frozen=True)
@@ -110,7 +111,7 @@ async def _send_or_edit_message(
reply_to: MessageRef | None = None,
notify: bool = True,
replace_ref: MessageRef | None = None,
thread_id: int | None = None,
thread_id: ThreadId | None = None,
) -> tuple[MessageRef | None, bool]:
msg = message
followups = message.extra.get("followups")
@@ -248,7 +249,7 @@ async def send_initial_progress(
tracker: ProgressTracker,
resume_formatter: Callable[[ResumeToken], str] | None = None,
context_line: str | None = None,
thread_id: int | None = None,
thread_id: ThreadId | None = None,
) -> ProgressMessageState:
progress_ref: MessageRef | None = None
last_rendered: RenderedMessage | None = None
@@ -358,7 +359,7 @@ async def send_result_message(
edit_ref: MessageRef | None,
replace_ref: MessageRef | None = None,
delete_tag: str = "final",
thread_id: int | None = None,
thread_id: ThreadId | None = None,
) -> None:
final_msg, edited = await _send_or_edit_message(
cfg.transport,
+7 -6
View File
@@ -8,16 +8,17 @@ import anyio
from .context import RunContext
from .model import ResumeToken
from .transport import ChannelId, MessageId, ThreadId
@dataclass(frozen=True, slots=True)
class ThreadJob:
chat_id: int
user_msg_id: int
chat_id: ChannelId
user_msg_id: MessageId
text: str
resume_token: ResumeToken
context: RunContext | None = None
thread_id: int | None = None
thread_id: ThreadId | None = None
RunJob = Callable[[ThreadJob], Awaitable[None]]
@@ -65,12 +66,12 @@ class ThreadScheduler:
async def enqueue_resume(
self,
chat_id: int,
user_msg_id: int,
chat_id: ChannelId,
user_msg_id: MessageId,
text: str,
resume_token: ResumeToken,
context: RunContext | None = None,
thread_id: int | None = None,
thread_id: ThreadId | None = None,
) -> None:
await self.enqueue(
ThreadJob(
+17 -1
View File
@@ -185,7 +185,11 @@ class TelegramTransport:
else None
)
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:
reply_to_message_id = cast(
int | None,
@@ -219,10 +223,16 @@ class TelegramTransport:
notify=notify,
)
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(
channel_id=chat_id,
message_id=message_id,
raw=sent,
thread_id=thread_id,
)
async def edit(
@@ -261,10 +271,16 @@ class TelegramTransport:
notify=notify,
)
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(
channel_id=chat_id,
message_id=message_id,
raw=edited,
thread_id=thread_id,
)
async def delete(self, *, ref: MessageRef) -> bool:
+19 -5
View File
@@ -1178,11 +1178,15 @@ class _CaptureTransport:
message: RenderedMessage,
options: SendOptions | None = None,
) -> 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)
self._next_id += 1
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(
self, *, ref: MessageRef, message: RenderedMessage, wait: bool = True
@@ -1218,7 +1222,11 @@ class _TelegramCommandExecutor(CommandExecutor):
self._chat_id = chat_id
self._user_msg_id = user_msg_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:
if request.context is not None:
@@ -1336,7 +1344,11 @@ async def _dispatch_command(
chat_id = msg.chat_id
user_msg_id = msg.message_id
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
else None
)
@@ -1349,7 +1361,9 @@ async def _dispatch_command(
user_msg_id=user_msg_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:
backend = get_command(command_id, allowlist=allowlist, required=False)
except ConfigError as exc:
+4 -3
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Awaitable, Callable
from dataclasses import dataclass
from functools import partial
from typing import cast
import anyio
from anyio.abc import TaskGroup
@@ -417,12 +418,12 @@ async def run_main_loop(
async def run_thread_job(job: ThreadJob) -> None:
await run_job(
job.chat_id,
job.user_msg_id,
cast(int, job.chat_id),
cast(int, job.user_msg_id),
job.text,
job.resume_token,
job.context,
job.thread_id,
cast(int | None, job.thread_id),
None,
scheduler.note_thread_known,
)
+3 -1
View File
@@ -5,6 +5,7 @@ from typing import Any, Protocol, TypeAlias
ChannelId: TypeAlias = int | str
MessageId: TypeAlias = int | str
ThreadId: TypeAlias = int | str
@dataclass(frozen=True, slots=True)
@@ -12,6 +13,7 @@ class MessageRef:
channel_id: ChannelId
message_id: MessageId
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)
@@ -25,7 +27,7 @@ class SendOptions:
reply_to: MessageRef | None = None
notify: bool = True
replace: MessageRef | None = None
thread_id: int | None = None
thread_id: ThreadId | None = None
class Transport(Protocol):