tele-mess-core 0.3.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.
- tele_mess_core/__init__.py +3 -0
- tele_mess_core/__main__.py +6 -0
- tele_mess_core/archive/__init__.py +4 -0
- tele_mess_core/archive/migrations.py +303 -0
- tele_mess_core/archive/schema.sql +410 -0
- tele_mess_core/archive/store.py +3268 -0
- tele_mess_core/cli.py +630 -0
- tele_mess_core/config.py +407 -0
- tele_mess_core/daily.py +3301 -0
- tele_mess_core/daily_jobs.py +516 -0
- tele_mess_core/logging_setup.py +32 -0
- tele_mess_core/maintenance.py +175 -0
- tele_mess_core/models.py +344 -0
- tele_mess_core/openai_fallback.py +331 -0
- tele_mess_core/runtime_paths.py +113 -0
- tele_mess_core/server/__init__.py +4 -0
- tele_mess_core/server/api.py +1540 -0
- tele_mess_core/server/console.html +1431 -0
- tele_mess_core/server/console.py +7 -0
- tele_mess_core/server/contracts.py +1573 -0
- tele_mess_core/telegram/__init__.py +6 -0
- tele_mess_core/telegram/auth.py +197 -0
- tele_mess_core/telegram/delivery.py +194 -0
- tele_mess_core/telegram/discovery.py +430 -0
- tele_mess_core/telegram/ingest.py +974 -0
- tele_mess_core/telegram/manager.py +469 -0
- tele_mess_core/telegram/runtime.py +84 -0
- tele_mess_core-0.3.0.dist-info/METADATA +365 -0
- tele_mess_core-0.3.0.dist-info/RECORD +33 -0
- tele_mess_core-0.3.0.dist-info/WHEEL +5 -0
- tele_mess_core-0.3.0.dist-info/entry_points.txt +2 -0
- tele_mess_core-0.3.0.dist-info/licenses/LICENSE +186 -0
- tele_mess_core-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Migration = Callable[[sqlite3.Connection], None]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def apply_migrations(connection: sqlite3.Connection, current_version: int, target_version: int) -> None:
|
|
11
|
+
if current_version > target_version:
|
|
12
|
+
raise RuntimeError(
|
|
13
|
+
f"Database schema version {current_version} is newer than supported version {target_version}"
|
|
14
|
+
)
|
|
15
|
+
for version in range(current_version + 1, target_version + 1):
|
|
16
|
+
migration = MIGRATIONS.get(version)
|
|
17
|
+
if migration is None:
|
|
18
|
+
raise RuntimeError(f"Missing database migration for version {version}")
|
|
19
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
20
|
+
try:
|
|
21
|
+
migration(connection)
|
|
22
|
+
connection.execute(f"PRAGMA user_version = {version}")
|
|
23
|
+
connection.execute(
|
|
24
|
+
"INSERT INTO meta(key, value) VALUES('schema_version', ?) "
|
|
25
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
26
|
+
(str(version),),
|
|
27
|
+
)
|
|
28
|
+
connection.commit()
|
|
29
|
+
except Exception:
|
|
30
|
+
connection.rollback()
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _migration_13(connection: sqlite3.Connection) -> None:
|
|
35
|
+
_ensure_column(connection, "daily_summary_jobs", "request_json", "TEXT")
|
|
36
|
+
_ensure_column(connection, "daily_summary_jobs", "dedupe_key", "TEXT")
|
|
37
|
+
_ensure_column(connection, "daily_summary_jobs", "worker_id", "TEXT")
|
|
38
|
+
_ensure_column(connection, "daily_summary_jobs", "lease_until", "TEXT")
|
|
39
|
+
_ensure_column(connection, "daily_summary_jobs", "heartbeat_at", "TEXT")
|
|
40
|
+
_ensure_column(connection, "daily_summary_jobs", "attempt", "INTEGER NOT NULL DEFAULT 0")
|
|
41
|
+
connection.execute(
|
|
42
|
+
"""
|
|
43
|
+
CREATE TABLE IF NOT EXISTS delivery_outbox (
|
|
44
|
+
outbox_id TEXT PRIMARY KEY,
|
|
45
|
+
summary_run_id TEXT NOT NULL,
|
|
46
|
+
job_id TEXT,
|
|
47
|
+
account_id TEXT NOT NULL,
|
|
48
|
+
origin_id INTEGER NOT NULL,
|
|
49
|
+
topic_id INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
chunk_index INTEGER NOT NULL,
|
|
51
|
+
chunk_count INTEGER NOT NULL,
|
|
52
|
+
content TEXT NOT NULL,
|
|
53
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
54
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
message_id INTEGER,
|
|
56
|
+
last_error TEXT,
|
|
57
|
+
next_attempt_at TEXT,
|
|
58
|
+
created_at TEXT NOT NULL,
|
|
59
|
+
updated_at TEXT NOT NULL,
|
|
60
|
+
sent_at TEXT,
|
|
61
|
+
UNIQUE(summary_run_id, account_id, origin_id, topic_id, chunk_index)
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
)
|
|
65
|
+
connection.execute(
|
|
66
|
+
"""
|
|
67
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_daily_summary_jobs_active_dedupe
|
|
68
|
+
ON daily_summary_jobs(dedupe_key)
|
|
69
|
+
WHERE dedupe_key IS NOT NULL
|
|
70
|
+
AND status IN ('queued', 'running', 'cancel_requested')
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
connection.execute(
|
|
74
|
+
"CREATE INDEX IF NOT EXISTS idx_daily_summary_jobs_claim ON daily_summary_jobs(status, lease_until, started_at)"
|
|
75
|
+
)
|
|
76
|
+
connection.execute(
|
|
77
|
+
"CREATE INDEX IF NOT EXISTS idx_delivery_outbox_pending ON delivery_outbox(status, next_attempt_at, created_at)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _migration_14(connection: sqlite3.Connection) -> None:
|
|
82
|
+
statements = (
|
|
83
|
+
"""
|
|
84
|
+
CREATE TRIGGER IF NOT EXISTS daily_summary_jobs_validate_insert
|
|
85
|
+
BEFORE INSERT ON daily_summary_jobs
|
|
86
|
+
WHEN NEW.status NOT IN ('queued', 'running', 'cancel_requested', 'completed', 'failed', 'canceled')
|
|
87
|
+
OR NEW.progress_total < 0
|
|
88
|
+
OR NEW.progress_current < 0
|
|
89
|
+
OR (NEW.progress_total > 0 AND NEW.progress_current > NEW.progress_total)
|
|
90
|
+
OR NEW.attempt < 0
|
|
91
|
+
BEGIN
|
|
92
|
+
SELECT RAISE(ABORT, 'invalid daily summary job state');
|
|
93
|
+
END
|
|
94
|
+
""",
|
|
95
|
+
"""
|
|
96
|
+
CREATE TRIGGER IF NOT EXISTS daily_summary_jobs_validate_update
|
|
97
|
+
BEFORE UPDATE ON daily_summary_jobs
|
|
98
|
+
WHEN NEW.status NOT IN ('queued', 'running', 'cancel_requested', 'completed', 'failed', 'canceled')
|
|
99
|
+
OR NEW.progress_total < 0
|
|
100
|
+
OR NEW.progress_current < 0
|
|
101
|
+
OR (NEW.progress_total > 0 AND NEW.progress_current > NEW.progress_total)
|
|
102
|
+
OR NEW.attempt < 0
|
|
103
|
+
BEGIN
|
|
104
|
+
SELECT RAISE(ABORT, 'invalid daily summary job state');
|
|
105
|
+
END
|
|
106
|
+
""",
|
|
107
|
+
"""
|
|
108
|
+
CREATE TRIGGER IF NOT EXISTS delivery_outbox_validate_insert
|
|
109
|
+
BEFORE INSERT ON delivery_outbox
|
|
110
|
+
WHEN NEW.status NOT IN ('pending', 'sending', 'retry', 'sent')
|
|
111
|
+
OR NEW.chunk_index <= 0
|
|
112
|
+
OR NEW.chunk_count <= 0
|
|
113
|
+
OR NEW.chunk_index > NEW.chunk_count
|
|
114
|
+
OR NEW.attempts < 0
|
|
115
|
+
BEGIN
|
|
116
|
+
SELECT RAISE(ABORT, 'invalid delivery outbox state');
|
|
117
|
+
END
|
|
118
|
+
""",
|
|
119
|
+
"""
|
|
120
|
+
CREATE TRIGGER IF NOT EXISTS delivery_outbox_validate_update
|
|
121
|
+
BEFORE UPDATE ON delivery_outbox
|
|
122
|
+
WHEN NEW.status NOT IN ('pending', 'sending', 'retry', 'sent')
|
|
123
|
+
OR NEW.chunk_index <= 0
|
|
124
|
+
OR NEW.chunk_count <= 0
|
|
125
|
+
OR NEW.chunk_index > NEW.chunk_count
|
|
126
|
+
OR NEW.attempts < 0
|
|
127
|
+
BEGIN
|
|
128
|
+
SELECT RAISE(ABORT, 'invalid delivery outbox state');
|
|
129
|
+
END
|
|
130
|
+
""",
|
|
131
|
+
)
|
|
132
|
+
for statement in statements:
|
|
133
|
+
connection.execute(statement)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _migration_15(connection: sqlite3.Connection) -> None:
|
|
137
|
+
connection.execute(
|
|
138
|
+
"""
|
|
139
|
+
CREATE TABLE IF NOT EXISTS daily_summary_delivery (
|
|
140
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
141
|
+
enabled INTEGER NOT NULL DEFAULT 0 CHECK (enabled IN (0, 1)),
|
|
142
|
+
account_id TEXT,
|
|
143
|
+
origin_id INTEGER,
|
|
144
|
+
topic_id INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
updated_at TEXT NOT NULL,
|
|
146
|
+
CHECK (enabled = 0 OR (account_id IS NOT NULL AND account_id != '' AND origin_id IS NOT NULL))
|
|
147
|
+
)
|
|
148
|
+
"""
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _migration_16(connection: sqlite3.Connection) -> None:
|
|
153
|
+
_ensure_column(connection, "daily_summary_records", "record_type", "TEXT NOT NULL DEFAULT 'summary'")
|
|
154
|
+
connection.execute(
|
|
155
|
+
"""
|
|
156
|
+
UPDATE daily_summary_records
|
|
157
|
+
SET record_type = CASE
|
|
158
|
+
WHEN content_json IS NOT NULL
|
|
159
|
+
AND content_json != ''
|
|
160
|
+
AND json_valid(content_json)
|
|
161
|
+
THEN COALESCE(CAST(json_extract(content_json, '$.record_type') AS TEXT), 'summary')
|
|
162
|
+
ELSE 'summary'
|
|
163
|
+
END
|
|
164
|
+
WHERE record_type = 'summary'
|
|
165
|
+
"""
|
|
166
|
+
)
|
|
167
|
+
connection.execute(
|
|
168
|
+
"""
|
|
169
|
+
CREATE TABLE IF NOT EXISTS daily_message_points (
|
|
170
|
+
point_id TEXT PRIMARY KEY,
|
|
171
|
+
run_id TEXT NOT NULL,
|
|
172
|
+
package_run_id TEXT NOT NULL,
|
|
173
|
+
date TEXT NOT NULL,
|
|
174
|
+
timezone TEXT NOT NULL,
|
|
175
|
+
source TEXT NOT NULL DEFAULT 'telegram',
|
|
176
|
+
account_id TEXT NOT NULL,
|
|
177
|
+
origin_id INTEGER NOT NULL,
|
|
178
|
+
topic_id INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
origin_title TEXT,
|
|
180
|
+
message_id INTEGER,
|
|
181
|
+
occurred_at TEXT NOT NULL,
|
|
182
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
183
|
+
tags_csv TEXT,
|
|
184
|
+
content TEXT NOT NULL CHECK (length(trim(content)) > 0),
|
|
185
|
+
telegram_deeplink TEXT,
|
|
186
|
+
permalink TEXT,
|
|
187
|
+
importance_score INTEGER NOT NULL DEFAULT 3 CHECK (importance_score BETWEEN 1 AND 5),
|
|
188
|
+
importance_reason TEXT,
|
|
189
|
+
origin_important INTEGER NOT NULL DEFAULT 0 CHECK (origin_important IN (0, 1)),
|
|
190
|
+
source_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
191
|
+
provider TEXT,
|
|
192
|
+
created_at TEXT NOT NULL,
|
|
193
|
+
updated_at TEXT NOT NULL
|
|
194
|
+
)
|
|
195
|
+
"""
|
|
196
|
+
)
|
|
197
|
+
connection.execute(
|
|
198
|
+
"CREATE INDEX IF NOT EXISTS idx_daily_summary_records_type_date "
|
|
199
|
+
"ON daily_summary_records(record_type, date, created_at)"
|
|
200
|
+
)
|
|
201
|
+
connection.execute(
|
|
202
|
+
"CREATE INDEX IF NOT EXISTS idx_daily_message_points_run "
|
|
203
|
+
"ON daily_message_points(run_id, occurred_at, point_id)"
|
|
204
|
+
)
|
|
205
|
+
connection.execute(
|
|
206
|
+
"CREATE INDEX IF NOT EXISTS idx_daily_message_points_lookup "
|
|
207
|
+
"ON daily_message_points(date, source, account_id, origin_id, topic_id, occurred_at)"
|
|
208
|
+
)
|
|
209
|
+
connection.execute(
|
|
210
|
+
"CREATE INDEX IF NOT EXISTS idx_daily_message_points_importance "
|
|
211
|
+
"ON daily_message_points(date, importance_score, occurred_at)"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _migration_17(connection: sqlite3.Connection) -> None:
|
|
216
|
+
_ensure_column(connection, "daily_summary_jobs", "retry_at", "TEXT")
|
|
217
|
+
_ensure_column(connection, "daily_summary_jobs", "retry_count", "INTEGER NOT NULL DEFAULT 0")
|
|
218
|
+
for table in ("daily_summary_jobs", "daily_summary_runs"):
|
|
219
|
+
columns = {str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
220
|
+
if not {"error", "progress_json"}.issubset(columns):
|
|
221
|
+
continue
|
|
222
|
+
connection.execute(
|
|
223
|
+
f"""
|
|
224
|
+
UPDATE {table}
|
|
225
|
+
SET error = 'Codex usage limit reached',
|
|
226
|
+
progress_json = CASE
|
|
227
|
+
WHEN progress_json IS NOT NULL AND json_valid(progress_json)
|
|
228
|
+
THEN json_set(
|
|
229
|
+
progress_json,
|
|
230
|
+
'$.error', 'Codex usage limit reached',
|
|
231
|
+
'$.error_kind', 'codex_usage_limit',
|
|
232
|
+
'$.retryable', 1
|
|
233
|
+
)
|
|
234
|
+
ELSE progress_json
|
|
235
|
+
END
|
|
236
|
+
WHERE error LIKE '%chatgpt.com/codex/settings/usage%'
|
|
237
|
+
OR error LIKE '%You''ve hit your usage limit%'
|
|
238
|
+
"""
|
|
239
|
+
)
|
|
240
|
+
connection.execute("DROP INDEX IF EXISTS idx_daily_summary_jobs_claim")
|
|
241
|
+
connection.execute(
|
|
242
|
+
"CREATE INDEX idx_daily_summary_jobs_claim "
|
|
243
|
+
"ON daily_summary_jobs(status, retry_at, lease_until, started_at)"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _migration_18(connection: sqlite3.Connection) -> None:
|
|
248
|
+
table = connection.execute(
|
|
249
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'capture_cursors'"
|
|
250
|
+
).fetchone()
|
|
251
|
+
if table is None:
|
|
252
|
+
return
|
|
253
|
+
_ensure_column(
|
|
254
|
+
connection,
|
|
255
|
+
"capture_cursors",
|
|
256
|
+
"history_scanned_through_id",
|
|
257
|
+
"INTEGER NOT NULL DEFAULT 0",
|
|
258
|
+
)
|
|
259
|
+
_ensure_column(
|
|
260
|
+
connection,
|
|
261
|
+
"capture_cursors",
|
|
262
|
+
"observed_max_message_id",
|
|
263
|
+
"INTEGER NOT NULL DEFAULT 0",
|
|
264
|
+
)
|
|
265
|
+
_ensure_column(connection, "capture_cursors", "backfill_head_message_id", "INTEGER")
|
|
266
|
+
_ensure_column(connection, "capture_cursors", "backfill_status", "TEXT")
|
|
267
|
+
_ensure_column(connection, "capture_cursors", "backfill_error", "TEXT")
|
|
268
|
+
_ensure_column(connection, "capture_cursors", "backfill_count", "INTEGER")
|
|
269
|
+
connection.execute(
|
|
270
|
+
"""
|
|
271
|
+
UPDATE capture_cursors
|
|
272
|
+
SET history_scanned_through_id = 0,
|
|
273
|
+
observed_max_message_id = MAX(
|
|
274
|
+
COALESCE(observed_max_message_id, 0),
|
|
275
|
+
COALESCE(last_message_id, 0)
|
|
276
|
+
),
|
|
277
|
+
last_message_id = MAX(
|
|
278
|
+
COALESCE(last_message_id, 0),
|
|
279
|
+
COALESCE(observed_max_message_id, 0)
|
|
280
|
+
),
|
|
281
|
+
backfill_status = CASE
|
|
282
|
+
WHEN COALESCE(last_message_id, 0) > 0 THEN 'migration_rescan'
|
|
283
|
+
ELSE backfill_status
|
|
284
|
+
END
|
|
285
|
+
"""
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _ensure_column(connection: sqlite3.Connection, table: str, column: str, definition: str) -> None:
|
|
290
|
+
rows = connection.execute(f"PRAGMA table_info({table})").fetchall()
|
|
291
|
+
names = {str(row[1]) for row in rows}
|
|
292
|
+
if column not in names:
|
|
293
|
+
connection.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
MIGRATIONS: dict[int, Migration] = {
|
|
297
|
+
13: _migration_13,
|
|
298
|
+
14: _migration_14,
|
|
299
|
+
15: _migration_15,
|
|
300
|
+
16: _migration_16,
|
|
301
|
+
17: _migration_17,
|
|
302
|
+
18: _migration_18,
|
|
303
|
+
}
|