codex-autorunner 0.1.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
5
6
|
import time
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Callable, Dict, Optional, Sequence
|
|
9
10
|
|
|
10
11
|
from ...core.logging_utils import log_event
|
|
12
|
+
from ...core.supervisor_utils import evict_lru_handle_locked, pop_idle_handles_locked
|
|
11
13
|
from ...workspace import canonical_workspace_root, workspace_id_for_path
|
|
12
14
|
from .client import ApprovalHandler, CodexAppServerClient, NotificationHandler
|
|
13
15
|
|
|
@@ -34,8 +36,17 @@ class WorkspaceAppServerSupervisor:
|
|
|
34
36
|
approval_handler: Optional[ApprovalHandler] = None,
|
|
35
37
|
notification_handler: Optional[NotificationHandler] = None,
|
|
36
38
|
logger: Optional[logging.Logger] = None,
|
|
37
|
-
auto_restart: bool =
|
|
39
|
+
auto_restart: Optional[bool] = None,
|
|
38
40
|
request_timeout: Optional[float] = None,
|
|
41
|
+
turn_stall_timeout_seconds: Optional[float] = None,
|
|
42
|
+
turn_stall_poll_interval_seconds: Optional[float] = None,
|
|
43
|
+
turn_stall_recovery_min_interval_seconds: Optional[float] = None,
|
|
44
|
+
max_message_bytes: Optional[int] = None,
|
|
45
|
+
oversize_preview_bytes: Optional[int] = None,
|
|
46
|
+
max_oversize_drain_bytes: Optional[int] = None,
|
|
47
|
+
restart_backoff_initial_seconds: Optional[float] = None,
|
|
48
|
+
restart_backoff_max_seconds: Optional[float] = None,
|
|
49
|
+
restart_backoff_jitter_ratio: Optional[float] = None,
|
|
39
50
|
default_approval_decision: str = "cancel",
|
|
40
51
|
max_handles: Optional[int] = None,
|
|
41
52
|
idle_ttl_seconds: Optional[float] = None,
|
|
@@ -46,8 +57,27 @@ class WorkspaceAppServerSupervisor:
|
|
|
46
57
|
self._approval_handler = approval_handler
|
|
47
58
|
self._notification_handler = notification_handler
|
|
48
59
|
self._logger = logger or logging.getLogger(__name__)
|
|
49
|
-
|
|
60
|
+
disable_restart_env = os.environ.get(
|
|
61
|
+
"CODEX_DISABLE_APP_SERVER_AUTORESTART_FOR_TESTS"
|
|
62
|
+
)
|
|
63
|
+
if disable_restart_env:
|
|
64
|
+
self._auto_restart = False
|
|
65
|
+
elif auto_restart is None:
|
|
66
|
+
self._auto_restart = True
|
|
67
|
+
else:
|
|
68
|
+
self._auto_restart = auto_restart
|
|
50
69
|
self._request_timeout = request_timeout
|
|
70
|
+
self._turn_stall_timeout_seconds = turn_stall_timeout_seconds
|
|
71
|
+
self._turn_stall_poll_interval_seconds = turn_stall_poll_interval_seconds
|
|
72
|
+
self._turn_stall_recovery_min_interval_seconds = (
|
|
73
|
+
turn_stall_recovery_min_interval_seconds
|
|
74
|
+
)
|
|
75
|
+
self._max_message_bytes = max_message_bytes
|
|
76
|
+
self._oversize_preview_bytes = oversize_preview_bytes
|
|
77
|
+
self._max_oversize_drain_bytes = max_oversize_drain_bytes
|
|
78
|
+
self._restart_backoff_initial_seconds = restart_backoff_initial_seconds
|
|
79
|
+
self._restart_backoff_max_seconds = restart_backoff_max_seconds
|
|
80
|
+
self._restart_backoff_jitter_ratio = restart_backoff_jitter_ratio
|
|
51
81
|
self._default_approval_decision = default_approval_decision
|
|
52
82
|
self._max_handles = max_handles
|
|
53
83
|
self._idle_ttl_seconds = idle_ttl_seconds
|
|
@@ -78,7 +108,8 @@ class WorkspaceAppServerSupervisor:
|
|
|
78
108
|
last_used_at=handle.last_used_at,
|
|
79
109
|
)
|
|
80
110
|
await handle.client.close()
|
|
81
|
-
except Exception:
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
self._logger.debug("Failed to close handle: %s", exc)
|
|
82
113
|
continue
|
|
83
114
|
|
|
84
115
|
async def prune_idle(self) -> int:
|
|
@@ -100,7 +131,8 @@ class WorkspaceAppServerSupervisor:
|
|
|
100
131
|
)
|
|
101
132
|
await handle.client.close()
|
|
102
133
|
closed += 1
|
|
103
|
-
except Exception:
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
self._logger.debug("Failed to prune handle: %s", exc)
|
|
104
136
|
continue
|
|
105
137
|
return closed
|
|
106
138
|
|
|
@@ -129,6 +161,15 @@ class WorkspaceAppServerSupervisor:
|
|
|
129
161
|
default_approval_decision=self._default_approval_decision,
|
|
130
162
|
auto_restart=self._auto_restart,
|
|
131
163
|
request_timeout=self._request_timeout,
|
|
164
|
+
turn_stall_timeout_seconds=self._turn_stall_timeout_seconds,
|
|
165
|
+
turn_stall_poll_interval_seconds=self._turn_stall_poll_interval_seconds,
|
|
166
|
+
turn_stall_recovery_min_interval_seconds=self._turn_stall_recovery_min_interval_seconds,
|
|
167
|
+
max_message_bytes=self._max_message_bytes,
|
|
168
|
+
oversize_preview_bytes=self._oversize_preview_bytes,
|
|
169
|
+
max_oversize_drain_bytes=self._max_oversize_drain_bytes,
|
|
170
|
+
restart_backoff_initial_seconds=self._restart_backoff_initial_seconds,
|
|
171
|
+
restart_backoff_max_seconds=self._restart_backoff_max_seconds,
|
|
172
|
+
restart_backoff_jitter_ratio=self._restart_backoff_jitter_ratio,
|
|
132
173
|
notification_handler=self._notification_handler,
|
|
133
174
|
logger=self._logger,
|
|
134
175
|
)
|
|
@@ -157,7 +198,8 @@ class WorkspaceAppServerSupervisor:
|
|
|
157
198
|
last_used_at=handle.last_used_at,
|
|
158
199
|
)
|
|
159
200
|
await handle.client.close()
|
|
160
|
-
except Exception:
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
self._logger.debug("Failed to close handle: %s", exc)
|
|
161
203
|
continue
|
|
162
204
|
return handle
|
|
163
205
|
|
|
@@ -173,35 +215,19 @@ class WorkspaceAppServerSupervisor:
|
|
|
173
215
|
return self._pop_idle_handles_locked()
|
|
174
216
|
|
|
175
217
|
def _pop_idle_handles_locked(self) -> list[AppServerHandle]:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
stale.append(handle)
|
|
184
|
-
return stale
|
|
218
|
+
return pop_idle_handles_locked(
|
|
219
|
+
self._handles,
|
|
220
|
+
self._idle_ttl_seconds,
|
|
221
|
+
self._logger,
|
|
222
|
+
"app_server",
|
|
223
|
+
last_used_at_getter=lambda h: h.last_used_at,
|
|
224
|
+
)
|
|
185
225
|
|
|
186
226
|
def _evict_lru_handle_locked(self) -> Optional[AppServerHandle]:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return None
|
|
191
|
-
lru_handle = min(
|
|
192
|
-
self._handles.values(),
|
|
193
|
-
key=lambda handle: handle.last_used_at or 0.0,
|
|
194
|
-
)
|
|
195
|
-
log_event(
|
|
227
|
+
return evict_lru_handle_locked(
|
|
228
|
+
self._handles,
|
|
229
|
+
self._max_handles,
|
|
196
230
|
self._logger,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
reason="max_handles",
|
|
200
|
-
workspace_id=lru_handle.workspace_id,
|
|
201
|
-
workspace_root=str(lru_handle.workspace_root),
|
|
202
|
-
max_handles=self._max_handles,
|
|
203
|
-
handle_count=len(self._handles),
|
|
204
|
-
last_used_at=lru_handle.last_used_at,
|
|
231
|
+
"app_server",
|
|
232
|
+
last_used_at_getter=lambda h: h.last_used_at or 0.0,
|
|
205
233
|
)
|
|
206
|
-
self._handles.pop(lru_handle.workspace_id, None)
|
|
207
|
-
return lru_handle
|
|
@@ -12,6 +12,16 @@ from ...core.circuit_breaker import CircuitBreaker
|
|
|
12
12
|
from ...core.exceptions import CodexError, PermanentError, TransientError
|
|
13
13
|
from ...core.logging_utils import log_event
|
|
14
14
|
from ...core.retry import retry_transient
|
|
15
|
+
from .api_schemas import (
|
|
16
|
+
TelegramAudioSchema,
|
|
17
|
+
TelegramDocumentSchema,
|
|
18
|
+
TelegramMessageEntitySchema,
|
|
19
|
+
TelegramPhotoSizeSchema,
|
|
20
|
+
TelegramVoiceSchema,
|
|
21
|
+
parse_callback_query_payload,
|
|
22
|
+
parse_message_payload,
|
|
23
|
+
parse_update_payload,
|
|
24
|
+
)
|
|
15
25
|
from .constants import TELEGRAM_CALLBACK_DATA_LIMIT, TELEGRAM_MAX_MESSAGE_LENGTH
|
|
16
26
|
from .retry import _extract_retry_after_seconds
|
|
17
27
|
|
|
@@ -120,6 +130,12 @@ class TelegramMessage:
|
|
|
120
130
|
voice: Optional[TelegramVoice] = None
|
|
121
131
|
media_group_id: Optional[str] = None
|
|
122
132
|
|
|
133
|
+
# Extra metadata used for trigger gating / UX (optional, depends on update payload).
|
|
134
|
+
chat_type: Optional[str] = None
|
|
135
|
+
reply_to_message_id: Optional[int] = None
|
|
136
|
+
reply_to_is_bot: bool = False
|
|
137
|
+
reply_to_username: Optional[str] = None
|
|
138
|
+
|
|
123
139
|
|
|
124
140
|
@dataclass(frozen=True)
|
|
125
141
|
class TelegramCallbackQuery:
|
|
@@ -227,12 +243,6 @@ class ReviewCommitCallback:
|
|
|
227
243
|
sha: str
|
|
228
244
|
|
|
229
245
|
|
|
230
|
-
@dataclass(frozen=True)
|
|
231
|
-
class PrFlowStartCallback:
|
|
232
|
-
slug: str
|
|
233
|
-
number: int
|
|
234
|
-
|
|
235
|
-
|
|
236
246
|
@dataclass(frozen=True)
|
|
237
247
|
class CancelCallback:
|
|
238
248
|
kind: str
|
|
@@ -323,111 +333,123 @@ def is_interrupt_alias(text: Optional[str]) -> bool:
|
|
|
323
333
|
|
|
324
334
|
|
|
325
335
|
def parse_update(update: dict[str, Any]) -> Optional[TelegramUpdate]:
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
try:
|
|
337
|
+
schema = parse_update_payload(update)
|
|
338
|
+
except Exception:
|
|
328
339
|
return None
|
|
329
|
-
message = _parse_message(update_id,
|
|
340
|
+
message = _parse_message(schema.update_id, schema.message, edited=False)
|
|
330
341
|
if message is None:
|
|
331
|
-
message = _parse_message(update_id,
|
|
332
|
-
callback = _parse_callback(update_id,
|
|
342
|
+
message = _parse_message(schema.update_id, schema.edited_message, edited=True)
|
|
343
|
+
callback = _parse_callback(schema.update_id, schema.callback_query)
|
|
333
344
|
if message is None and callback is None:
|
|
334
345
|
return None
|
|
335
|
-
return TelegramUpdate(
|
|
346
|
+
return TelegramUpdate(
|
|
347
|
+
update_id=schema.update_id, message=message, callback=callback
|
|
348
|
+
)
|
|
336
349
|
|
|
337
350
|
|
|
338
351
|
def _parse_message(
|
|
339
352
|
update_id: int, payload: Any, *, edited: bool = False
|
|
340
353
|
) -> Optional[TelegramMessage]:
|
|
341
|
-
|
|
354
|
+
schema = parse_message_payload(payload)
|
|
355
|
+
if schema is None:
|
|
342
356
|
return None
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if not isinstance(message_id, int) or not isinstance(chat, dict):
|
|
346
|
-
return None
|
|
347
|
-
chat_id = chat.get("id")
|
|
357
|
+
|
|
358
|
+
chat_id = schema.chat.get("id") if isinstance(schema.chat, dict) else None
|
|
348
359
|
if not isinstance(chat_id, int):
|
|
349
360
|
return None
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
|
|
362
|
+
chat_type = schema.chat.get("type") if isinstance(schema.chat, dict) else None
|
|
363
|
+
if chat_type is not None and not isinstance(chat_type, str):
|
|
364
|
+
chat_type = None
|
|
365
|
+
|
|
366
|
+
reply_to_message_id: Optional[int] = None
|
|
367
|
+
reply_to_is_bot = False
|
|
368
|
+
reply_to_username: Optional[str] = None
|
|
369
|
+
if isinstance(schema.reply_to_message, dict):
|
|
370
|
+
rmid = schema.reply_to_message.get("message_id")
|
|
371
|
+
if isinstance(rmid, int):
|
|
372
|
+
reply_to_message_id = rmid
|
|
373
|
+
reply_from = schema.reply_to_message.get("from")
|
|
374
|
+
if isinstance(reply_from, dict):
|
|
375
|
+
is_bot = reply_from.get("is_bot")
|
|
376
|
+
if isinstance(is_bot, bool):
|
|
377
|
+
reply_to_is_bot = is_bot
|
|
378
|
+
username = reply_from.get("username")
|
|
379
|
+
if isinstance(username, str):
|
|
380
|
+
reply_to_username = username
|
|
381
|
+
|
|
382
|
+
from_user_id = (
|
|
383
|
+
schema.from_user.get("id") if isinstance(schema.from_user, dict) else None
|
|
384
|
+
)
|
|
355
385
|
if from_user_id is not None and not isinstance(from_user_id, int):
|
|
356
386
|
from_user_id = None
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
photos = _parse_photo_sizes(payload.get("photo"))
|
|
366
|
-
document = _parse_document(payload.get("document"))
|
|
367
|
-
audio = _parse_audio(payload.get("audio"))
|
|
368
|
-
voice = _parse_voice(payload.get("voice"))
|
|
369
|
-
media_group_id = payload.get("media_group_id")
|
|
370
|
-
if media_group_id is not None and not isinstance(media_group_id, str):
|
|
371
|
-
media_group_id = None
|
|
372
|
-
date = payload.get("date")
|
|
373
|
-
if date is not None and not isinstance(date, int):
|
|
374
|
-
date = None
|
|
375
|
-
is_topic_message = bool(payload.get("is_topic_message"))
|
|
387
|
+
|
|
388
|
+
entities = _parse_entities(schema.entities)
|
|
389
|
+
caption_entities = _parse_entities(schema.caption_entities)
|
|
390
|
+
photos = _parse_photo_sizes(schema.photo)
|
|
391
|
+
document = _parse_document(schema.document)
|
|
392
|
+
audio = _parse_audio(schema.audio)
|
|
393
|
+
voice = _parse_voice(schema.voice)
|
|
394
|
+
|
|
376
395
|
return TelegramMessage(
|
|
377
396
|
update_id=update_id,
|
|
378
|
-
message_id=message_id,
|
|
397
|
+
message_id=schema.message_id,
|
|
379
398
|
chat_id=chat_id,
|
|
380
|
-
thread_id=
|
|
399
|
+
thread_id=schema.message_thread_id,
|
|
381
400
|
from_user_id=from_user_id,
|
|
382
|
-
text=text,
|
|
383
|
-
date=date,
|
|
384
|
-
is_topic_message=is_topic_message,
|
|
401
|
+
text=schema.text,
|
|
402
|
+
date=schema.date,
|
|
403
|
+
is_topic_message=schema.is_topic_message,
|
|
385
404
|
is_edited=edited,
|
|
386
|
-
caption=caption,
|
|
405
|
+
caption=schema.caption,
|
|
387
406
|
entities=entities,
|
|
388
407
|
caption_entities=caption_entities,
|
|
389
408
|
photos=photos,
|
|
390
409
|
document=document,
|
|
391
410
|
audio=audio,
|
|
392
411
|
voice=voice,
|
|
393
|
-
media_group_id=media_group_id,
|
|
412
|
+
media_group_id=schema.media_group_id,
|
|
413
|
+
chat_type=chat_type,
|
|
414
|
+
reply_to_message_id=reply_to_message_id,
|
|
415
|
+
reply_to_is_bot=reply_to_is_bot,
|
|
416
|
+
reply_to_username=reply_to_username,
|
|
394
417
|
)
|
|
395
418
|
|
|
396
419
|
|
|
397
420
|
def _parse_callback(update_id: int, payload: Any) -> Optional[TelegramCallbackQuery]:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
callback_id = payload.get("id")
|
|
401
|
-
if not isinstance(callback_id, str):
|
|
421
|
+
schema = parse_callback_query_payload(payload)
|
|
422
|
+
if schema is None:
|
|
402
423
|
return None
|
|
403
|
-
|
|
404
|
-
from_user_id =
|
|
424
|
+
|
|
425
|
+
from_user_id = (
|
|
426
|
+
schema.from_user.get("id") if isinstance(schema.from_user, dict) else None
|
|
427
|
+
)
|
|
405
428
|
if from_user_id is not None and not isinstance(from_user_id, int):
|
|
406
429
|
from_user_id = None
|
|
407
|
-
|
|
408
|
-
if data is not None and not isinstance(data, str):
|
|
409
|
-
data = None
|
|
410
|
-
message = payload.get("message")
|
|
430
|
+
|
|
411
431
|
message_id = None
|
|
412
432
|
chat_id = None
|
|
413
433
|
thread_id = None
|
|
414
|
-
if isinstance(message, dict):
|
|
415
|
-
message_id = message.get("message_id")
|
|
416
|
-
chat = message.get("chat")
|
|
434
|
+
if isinstance(schema.message, dict):
|
|
435
|
+
message_id = schema.message.get("message_id")
|
|
436
|
+
chat = schema.message.get("chat")
|
|
417
437
|
if isinstance(chat, dict):
|
|
418
438
|
chat_id = chat.get("id")
|
|
419
|
-
thread_id = message.get("message_thread_id")
|
|
439
|
+
thread_id = schema.message.get("message_thread_id")
|
|
440
|
+
|
|
420
441
|
if message_id is not None and not isinstance(message_id, int):
|
|
421
442
|
message_id = None
|
|
422
443
|
if chat_id is not None and not isinstance(chat_id, int):
|
|
423
444
|
chat_id = None
|
|
424
445
|
if thread_id is not None and not isinstance(thread_id, int):
|
|
425
446
|
thread_id = None
|
|
447
|
+
|
|
426
448
|
return TelegramCallbackQuery(
|
|
427
449
|
update_id=update_id,
|
|
428
|
-
callback_id=
|
|
450
|
+
callback_id=schema.id,
|
|
429
451
|
from_user_id=from_user_id,
|
|
430
|
-
data=data,
|
|
452
|
+
data=schema.data,
|
|
431
453
|
message_id=message_id,
|
|
432
454
|
chat_id=chat_id,
|
|
433
455
|
thread_id=thread_id,
|
|
@@ -441,26 +463,17 @@ def _parse_photo_sizes(payload: Any) -> tuple[TelegramPhotoSize, ...]:
|
|
|
441
463
|
for item in payload:
|
|
442
464
|
if not isinstance(item, dict):
|
|
443
465
|
continue
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
file_unique_id = item.get("file_unique_id")
|
|
448
|
-
if file_unique_id is not None and not isinstance(file_unique_id, str):
|
|
449
|
-
file_unique_id = None
|
|
450
|
-
width = item.get("width")
|
|
451
|
-
height = item.get("height")
|
|
452
|
-
if not isinstance(width, int) or not isinstance(height, int):
|
|
466
|
+
try:
|
|
467
|
+
schema = TelegramPhotoSizeSchema.model_validate(item)
|
|
468
|
+
except Exception:
|
|
453
469
|
continue
|
|
454
|
-
file_size = item.get("file_size")
|
|
455
|
-
if file_size is not None and not isinstance(file_size, int):
|
|
456
|
-
file_size = None
|
|
457
470
|
sizes.append(
|
|
458
471
|
TelegramPhotoSize(
|
|
459
|
-
file_id=file_id,
|
|
460
|
-
file_unique_id=file_unique_id,
|
|
461
|
-
width=width,
|
|
462
|
-
height=height,
|
|
463
|
-
file_size=file_size,
|
|
472
|
+
file_id=schema.file_id,
|
|
473
|
+
file_unique_id=schema.file_unique_id,
|
|
474
|
+
width=schema.width,
|
|
475
|
+
height=schema.height,
|
|
476
|
+
file_size=schema.file_size,
|
|
464
477
|
)
|
|
465
478
|
)
|
|
466
479
|
return tuple(sizes)
|
|
@@ -469,85 +482,49 @@ def _parse_photo_sizes(payload: Any) -> tuple[TelegramPhotoSize, ...]:
|
|
|
469
482
|
def _parse_document(payload: Any) -> Optional[TelegramDocument]:
|
|
470
483
|
if not isinstance(payload, dict):
|
|
471
484
|
return None
|
|
472
|
-
|
|
473
|
-
|
|
485
|
+
try:
|
|
486
|
+
schema = TelegramDocumentSchema.model_validate(payload)
|
|
487
|
+
except Exception:
|
|
474
488
|
return None
|
|
475
|
-
file_unique_id = payload.get("file_unique_id")
|
|
476
|
-
if file_unique_id is not None and not isinstance(file_unique_id, str):
|
|
477
|
-
file_unique_id = None
|
|
478
|
-
file_name = payload.get("file_name")
|
|
479
|
-
if file_name is not None and not isinstance(file_name, str):
|
|
480
|
-
file_name = None
|
|
481
|
-
mime_type = payload.get("mime_type")
|
|
482
|
-
if mime_type is not None and not isinstance(mime_type, str):
|
|
483
|
-
mime_type = None
|
|
484
|
-
file_size = payload.get("file_size")
|
|
485
|
-
if file_size is not None and not isinstance(file_size, int):
|
|
486
|
-
file_size = None
|
|
487
489
|
return TelegramDocument(
|
|
488
|
-
file_id=file_id,
|
|
489
|
-
file_unique_id=file_unique_id,
|
|
490
|
-
file_name=file_name,
|
|
491
|
-
mime_type=mime_type,
|
|
492
|
-
file_size=file_size,
|
|
490
|
+
file_id=schema.file_id,
|
|
491
|
+
file_unique_id=schema.file_unique_id,
|
|
492
|
+
file_name=schema.file_name,
|
|
493
|
+
mime_type=schema.mime_type,
|
|
494
|
+
file_size=schema.file_size,
|
|
493
495
|
)
|
|
494
496
|
|
|
495
497
|
|
|
496
498
|
def _parse_audio(payload: Any) -> Optional[TelegramAudio]:
|
|
497
499
|
if not isinstance(payload, dict):
|
|
498
500
|
return None
|
|
499
|
-
|
|
500
|
-
|
|
501
|
+
try:
|
|
502
|
+
schema = TelegramAudioSchema.model_validate(payload)
|
|
503
|
+
except Exception:
|
|
501
504
|
return None
|
|
502
|
-
file_unique_id = payload.get("file_unique_id")
|
|
503
|
-
if file_unique_id is not None and not isinstance(file_unique_id, str):
|
|
504
|
-
file_unique_id = None
|
|
505
|
-
duration = payload.get("duration")
|
|
506
|
-
if duration is not None and not isinstance(duration, int):
|
|
507
|
-
duration = None
|
|
508
|
-
file_name = payload.get("file_name")
|
|
509
|
-
if file_name is not None and not isinstance(file_name, str):
|
|
510
|
-
file_name = None
|
|
511
|
-
mime_type = payload.get("mime_type")
|
|
512
|
-
if mime_type is not None and not isinstance(mime_type, str):
|
|
513
|
-
mime_type = None
|
|
514
|
-
file_size = payload.get("file_size")
|
|
515
|
-
if file_size is not None and not isinstance(file_size, int):
|
|
516
|
-
file_size = None
|
|
517
505
|
return TelegramAudio(
|
|
518
|
-
file_id=file_id,
|
|
519
|
-
file_unique_id=file_unique_id,
|
|
520
|
-
duration=duration,
|
|
521
|
-
file_name=file_name,
|
|
522
|
-
mime_type=mime_type,
|
|
523
|
-
file_size=file_size,
|
|
506
|
+
file_id=schema.file_id,
|
|
507
|
+
file_unique_id=schema.file_unique_id,
|
|
508
|
+
duration=schema.duration,
|
|
509
|
+
file_name=schema.file_name,
|
|
510
|
+
mime_type=schema.mime_type,
|
|
511
|
+
file_size=schema.file_size,
|
|
524
512
|
)
|
|
525
513
|
|
|
526
514
|
|
|
527
515
|
def _parse_voice(payload: Any) -> Optional[TelegramVoice]:
|
|
528
516
|
if not isinstance(payload, dict):
|
|
529
517
|
return None
|
|
530
|
-
|
|
531
|
-
|
|
518
|
+
try:
|
|
519
|
+
schema = TelegramVoiceSchema.model_validate(payload)
|
|
520
|
+
except Exception:
|
|
532
521
|
return None
|
|
533
|
-
file_unique_id = payload.get("file_unique_id")
|
|
534
|
-
if file_unique_id is not None and not isinstance(file_unique_id, str):
|
|
535
|
-
file_unique_id = None
|
|
536
|
-
duration = payload.get("duration")
|
|
537
|
-
if duration is not None and not isinstance(duration, int):
|
|
538
|
-
duration = None
|
|
539
|
-
mime_type = payload.get("mime_type")
|
|
540
|
-
if mime_type is not None and not isinstance(mime_type, str):
|
|
541
|
-
mime_type = None
|
|
542
|
-
file_size = payload.get("file_size")
|
|
543
|
-
if file_size is not None and not isinstance(file_size, int):
|
|
544
|
-
file_size = None
|
|
545
522
|
return TelegramVoice(
|
|
546
|
-
file_id=file_id,
|
|
547
|
-
file_unique_id=file_unique_id,
|
|
548
|
-
duration=duration,
|
|
549
|
-
mime_type=mime_type,
|
|
550
|
-
file_size=file_size,
|
|
523
|
+
file_id=schema.file_id,
|
|
524
|
+
file_unique_id=schema.file_unique_id,
|
|
525
|
+
duration=schema.duration,
|
|
526
|
+
mime_type=schema.mime_type,
|
|
527
|
+
file_size=schema.file_size,
|
|
551
528
|
)
|
|
552
529
|
|
|
553
530
|
|
|
@@ -558,14 +535,15 @@ def _parse_entities(payload: Any) -> tuple[TelegramMessageEntity, ...]:
|
|
|
558
535
|
for item in payload:
|
|
559
536
|
if not isinstance(item, dict):
|
|
560
537
|
continue
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if not isinstance(kind, str):
|
|
565
|
-
continue
|
|
566
|
-
if not isinstance(offset, int) or not isinstance(length, int):
|
|
538
|
+
try:
|
|
539
|
+
schema = TelegramMessageEntitySchema.model_validate(item)
|
|
540
|
+
except Exception:
|
|
567
541
|
continue
|
|
568
|
-
entities.append(
|
|
542
|
+
entities.append(
|
|
543
|
+
TelegramMessageEntity(
|
|
544
|
+
type=schema.type, offset=schema.offset, length=schema.length
|
|
545
|
+
)
|
|
546
|
+
)
|
|
569
547
|
return tuple(entities)
|
|
570
548
|
|
|
571
549
|
|
|
@@ -757,12 +735,6 @@ def encode_page_callback(kind: str, page: int) -> str:
|
|
|
757
735
|
return data
|
|
758
736
|
|
|
759
737
|
|
|
760
|
-
def encode_pr_flow_start_callback(slug: str, number: int) -> str:
|
|
761
|
-
data = f"pr_flow_start:{slug}#{number}"
|
|
762
|
-
_validate_callback_data(data)
|
|
763
|
-
return data
|
|
764
|
-
|
|
765
|
-
|
|
766
738
|
def encode_compact_callback(action: str) -> str:
|
|
767
739
|
data = f"compact:{action}"
|
|
768
740
|
_validate_callback_data(data)
|
|
@@ -786,7 +758,6 @@ def parse_callback_data(
|
|
|
786
758
|
UpdateCallback,
|
|
787
759
|
UpdateConfirmCallback,
|
|
788
760
|
ReviewCommitCallback,
|
|
789
|
-
PrFlowStartCallback,
|
|
790
761
|
CancelCallback,
|
|
791
762
|
CompactCallback,
|
|
792
763
|
PageCallback,
|
|
@@ -868,16 +839,6 @@ def parse_callback_data(
|
|
|
868
839
|
if not sha:
|
|
869
840
|
return None
|
|
870
841
|
return ReviewCommitCallback(sha=sha)
|
|
871
|
-
if data.startswith("pr_flow_start:"):
|
|
872
|
-
_, _, rest = data.partition(":")
|
|
873
|
-
if not rest:
|
|
874
|
-
return None
|
|
875
|
-
if "#" not in rest:
|
|
876
|
-
return None
|
|
877
|
-
slug, _, number_str = rest.partition("#")
|
|
878
|
-
if not slug or not number_str or not number_str.isdigit():
|
|
879
|
-
return None
|
|
880
|
-
return PrFlowStartCallback(slug=slug, number=int(number_str))
|
|
881
842
|
if data.startswith("cancel:"):
|
|
882
843
|
_, _, kind = data.partition(":")
|
|
883
844
|
if not kind:
|
|
@@ -1447,6 +1408,7 @@ class TelegramBotClient:
|
|
|
1447
1408
|
message_id: int,
|
|
1448
1409
|
text: str,
|
|
1449
1410
|
*,
|
|
1411
|
+
message_thread_id: Optional[int] = None,
|
|
1450
1412
|
reply_markup: Optional[dict[str, Any]] = None,
|
|
1451
1413
|
parse_mode: Optional[str] = None,
|
|
1452
1414
|
disable_web_page_preview: bool = True,
|
|
@@ -1456,6 +1418,7 @@ class TelegramBotClient:
|
|
|
1456
1418
|
logging.INFO,
|
|
1457
1419
|
"telegram.edit_message",
|
|
1458
1420
|
chat_id=chat_id,
|
|
1421
|
+
thread_id=message_thread_id,
|
|
1459
1422
|
message_id=message_id,
|
|
1460
1423
|
text_len=len(text),
|
|
1461
1424
|
has_markup=reply_markup is not None,
|
|
@@ -1468,6 +1431,8 @@ class TelegramBotClient:
|
|
|
1468
1431
|
"text": text,
|
|
1469
1432
|
"disable_web_page_preview": disable_web_page_preview,
|
|
1470
1433
|
}
|
|
1434
|
+
if message_thread_id is not None:
|
|
1435
|
+
payload["message_thread_id"] = message_thread_id
|
|
1471
1436
|
if reply_markup is not None:
|
|
1472
1437
|
payload["reply_markup"] = reply_markup
|
|
1473
1438
|
if parse_mode is not None:
|
|
@@ -1479,12 +1444,15 @@ class TelegramBotClient:
|
|
|
1479
1444
|
self,
|
|
1480
1445
|
chat_id: Union[int, str],
|
|
1481
1446
|
message_id: int,
|
|
1447
|
+
*,
|
|
1448
|
+
message_thread_id: Optional[int] = None,
|
|
1482
1449
|
) -> bool:
|
|
1483
1450
|
log_event(
|
|
1484
1451
|
self._logger,
|
|
1485
1452
|
logging.INFO,
|
|
1486
1453
|
"telegram.delete_message",
|
|
1487
1454
|
chat_id=chat_id,
|
|
1455
|
+
thread_id=message_thread_id,
|
|
1488
1456
|
message_id=message_id,
|
|
1489
1457
|
)
|
|
1490
1458
|
payload: dict[str, Any] = {"chat_id": chat_id, "message_id": message_id}
|
|
@@ -1495,6 +1463,9 @@ class TelegramBotClient:
|
|
|
1495
1463
|
self,
|
|
1496
1464
|
callback_query_id: str,
|
|
1497
1465
|
*,
|
|
1466
|
+
chat_id: Optional[int] = None,
|
|
1467
|
+
thread_id: Optional[int] = None,
|
|
1468
|
+
message_id: Optional[int] = None,
|
|
1498
1469
|
text: Optional[str] = None,
|
|
1499
1470
|
show_alert: bool = False,
|
|
1500
1471
|
) -> dict[str, Any]:
|
|
@@ -1503,6 +1474,9 @@ class TelegramBotClient:
|
|
|
1503
1474
|
logging.INFO,
|
|
1504
1475
|
"telegram.answer_callback",
|
|
1505
1476
|
callback_query_id=callback_query_id,
|
|
1477
|
+
chat_id=chat_id,
|
|
1478
|
+
thread_id=thread_id,
|
|
1479
|
+
message_id=message_id,
|
|
1506
1480
|
text_len=len(text) if text else 0,
|
|
1507
1481
|
show_alert=show_alert,
|
|
1508
1482
|
)
|