telepost-bot 2.78.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.
Files changed (165) hide show
  1. config/__init__.py +3 -0
  2. config/fly-telepost-policy.example.json +16 -0
  3. config/review_policy.md +64 -0
  4. config/settings.py +425 -0
  5. database/__init__.py +3 -0
  6. database/db_manager.py +1057 -0
  7. handlers/__init__.py +15 -0
  8. handlers/api_commands.py +73 -0
  9. handlers/automation.py +438 -0
  10. handlers/botconfig.py +255 -0
  11. handlers/callback_handlers.py +679 -0
  12. handlers/channel_listener.py +923 -0
  13. handlers/command_handlers.py +615 -0
  14. handlers/conversation.py +74 -0
  15. handlers/error_handler.py +99 -0
  16. handlers/index_handlers.py +201 -0
  17. handlers/mode_selection.py +116 -0
  18. handlers/moderation.py +145 -0
  19. handlers/preview_handlers.py +286 -0
  20. handlers/publish.py +1399 -0
  21. handlers/reaction_stats.py +240 -0
  22. handlers/recovery.py +89 -0
  23. handlers/review.py +2107 -0
  24. handlers/schedule_status.py +82 -0
  25. handlers/search_handlers.py +987 -0
  26. handlers/stats_handlers.py +439 -0
  27. handlers/upload.py +132 -0
  28. main.py +939 -0
  29. mcp_server/__init__.py +1 -0
  30. mcp_server/review_client.py +151 -0
  31. mcp_server/server.py +182 -0
  32. models/__init__.py +3 -0
  33. models/state.py +13 -0
  34. protocol/__init__.py +0 -0
  35. protocol/v1/SOURCES.sha256 +15 -0
  36. protocol/v1/__init__.py +0 -0
  37. protocol/v1/error-mapping.json +46 -0
  38. protocol/v1/fixtures/ackresult.candidate_search.json +6 -0
  39. protocol/v1/fixtures/capabilities.pixivflow.json +27 -0
  40. protocol/v1/fixtures/event.job.expired.json +13 -0
  41. protocol/v1/fixtures/event.job.succeeded.json +13 -0
  42. protocol/v1/fixtures/eventpage.candidate_search.json +26 -0
  43. protocol/v1/fixtures/job.cancelled.candidate_search.json +24 -0
  44. protocol/v1/fixtures/job.failed.no_candidate.json +30 -0
  45. protocol/v1/fixtures/job.queued.json +19 -0
  46. protocol/v1/fixtures/job.running.json +19 -0
  47. protocol/v1/fixtures/job.succeeded.candidate_search.json +54 -0
  48. protocol/v1/fixtures/jobpage.candidate_search.json +25 -0
  49. protocol/v1/fixtures/task.candidate_search.json +19 -0
  50. protocol/v1/fixtures/task.candidate_search.target_only.json +10 -0
  51. protocol/v1/protocol.schema.json +352 -0
  52. run.py +869 -0
  53. services/__init__.py +1 -0
  54. services/review_service.py +1120 -0
  55. telepost/__init__.py +0 -0
  56. telepost/api/__init__.py +0 -0
  57. telepost/application/__init__.py +0 -0
  58. telepost/application/admin_ops.py +333 -0
  59. telepost/application/automation.py +131 -0
  60. telepost/application/delivery_planner.py +245 -0
  61. telepost/application/editorial.py +200 -0
  62. telepost/application/hot.py +253 -0
  63. telepost/application/novel_fallback_card.py +145 -0
  64. telepost/application/novel_preview.py +289 -0
  65. telepost/application/pixivflow_jobs.py +1191 -0
  66. telepost/application/post_media.py +136 -0
  67. telepost/application/posts.py +118 -0
  68. telepost/application/publication.py +712 -0
  69. telepost/application/reaction_backfill.py +104 -0
  70. telepost/application/recovery.py +281 -0
  71. telepost/application/refetch.py +453 -0
  72. telepost/application/rerender.py +275 -0
  73. telepost/application/resubmit.py +252 -0
  74. telepost/application/review_queue.py +944 -0
  75. telepost/application/submitter_notify.py +337 -0
  76. telepost/application/telepress_provider.py +170 -0
  77. telepost/build_info.py +39 -0
  78. telepost/domain/__init__.py +0 -0
  79. telepost/domain/automation.py +168 -0
  80. telepost/domain/delivery.py +248 -0
  81. telepost/domain/editorial.py +196 -0
  82. telepost/domain/errors.py +74 -0
  83. telepost/domain/hot.py +140 -0
  84. telepost/domain/media.py +120 -0
  85. telepost/domain/navigation.py +99 -0
  86. telepost/domain/novel_preview.py +137 -0
  87. telepost/domain/packing.py +51 -0
  88. telepost/domain/presentation.py +161 -0
  89. telepost/domain/presentation_policy.py +175 -0
  90. telepost/domain/refetch_state.py +316 -0
  91. telepost/domain/review.py +75 -0
  92. telepost/domain/submission.py +118 -0
  93. telepost/miniapp/__init__.py +6 -0
  94. telepost/miniapp/auth.py +137 -0
  95. telepost/miniapp/rbac.py +102 -0
  96. telepost/miniapp/role_bindings.py +131 -0
  97. telepost/miniapp/session.py +181 -0
  98. telepost/observability/__init__.py +1 -0
  99. telepost/observability/audit.py +123 -0
  100. telepost/observability/cli.py +148 -0
  101. telepost/observability/doctor.py +1180 -0
  102. telepost/observability/errors.py +51 -0
  103. telepost/observability/redact.py +76 -0
  104. telepost/storage/__init__.py +0 -0
  105. telepost/storage/sqlite/__init__.py +0 -0
  106. telepost/storage/sqlite/automation.py +185 -0
  107. telepost/storage/sqlite/columns.py +18 -0
  108. telepost/storage/sqlite/editorial.py +226 -0
  109. telepost/storage/sqlite/ledger.py +150 -0
  110. telepost/storage/sqlite/media_assets.py +141 -0
  111. telepost/storage/sqlite/moderation.py +82 -0
  112. telepost/storage/sqlite/novel_preview.py +91 -0
  113. telepost/storage/sqlite/posts.py +68 -0
  114. telepost/storage/sqlite/protocol_events.py +192 -0
  115. telepost/storage/sqlite/recovery.py +81 -0
  116. telepost/storage/sqlite/refetch.py +797 -0
  117. telepost/storage/sqlite/reviews.py +802 -0
  118. telepost/storage/sqlite/submitter_notifications.py +189 -0
  119. telepost/telegram/__init__.py +0 -0
  120. telepost/telegram/capture.py +35 -0
  121. telepost/telegram/delivery/__init__.py +0 -0
  122. telepost/telegram/delivery/discussion.py +329 -0
  123. telepost/telegram/delivery/executor.py +369 -0
  124. telepost/telegram/delivery/gateway.py +107 -0
  125. telepost/telegram/delivery/legacy_runner.py +181 -0
  126. telepost/telegram/delivery/planner.py +120 -0
  127. telepost/telegram/delivery/preparation.py +1023 -0
  128. telepost/telegram/delivery/registry.py +112 -0
  129. telepost/telegram/delivery/sender.py +255 -0
  130. telepost/telegram/review_keyboard.py +266 -0
  131. telepost/telegram/review_stager.py +747 -0
  132. telepost_bot-2.78.0.dist-info/METADATA +164 -0
  133. telepost_bot-2.78.0.dist-info/RECORD +165 -0
  134. telepost_bot-2.78.0.dist-info/WHEEL +5 -0
  135. telepost_bot-2.78.0.dist-info/entry_points.txt +2 -0
  136. telepost_bot-2.78.0.dist-info/licenses/LICENSE +21 -0
  137. telepost_bot-2.78.0.dist-info/top_level.txt +13 -0
  138. ui/__init__.py +0 -0
  139. ui/keyboards.py +281 -0
  140. ui/messages.py +512 -0
  141. utils/__init__.py +3 -0
  142. utils/api_server.py +3110 -0
  143. utils/api_tokens.py +62 -0
  144. utils/blacklist.py +188 -0
  145. utils/cache.py +51 -0
  146. utils/config_wizard.py +92 -0
  147. utils/database.py +201 -0
  148. utils/file_validator.py +173 -0
  149. utils/heat_calculator.py +184 -0
  150. utils/helper_functions.py +662 -0
  151. utils/index_manager.py +477 -0
  152. utils/logging_config.py +124 -0
  153. utils/maintenance_jobs.py +71 -0
  154. utils/polling_server.py +85 -0
  155. utils/run_mode.py +36 -0
  156. utils/runtime_policy.py +86 -0
  157. utils/schedule_watchdog.py +77 -0
  158. utils/search_engine.py +515 -0
  159. utils/submission.py +105 -0
  160. utils/webhook_server.py +334 -0
  161. webapp/__init__.py +4 -0
  162. webapp/dist/assets/2d82b92e720462f8dd3b-Bh4Fahcw.svg +372 -0
  163. webapp/dist/assets/index-C-xxEN9W.js +110 -0
  164. webapp/dist/assets/index-DJgL81We.css +1 -0
  165. webapp/dist/index.html +15 -0
config/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ 配置模块
3
+ """
@@ -0,0 +1,16 @@
1
+ {
2
+ "bots": {
3
+ "1": {
4
+ "channelId": "-1001111111111",
5
+ "reviewChatId": "-1002222222222",
6
+ "apiReviewRequired": true,
7
+ "chatReviewRequired": false
8
+ },
9
+ "2": {
10
+ "channelId": "-1003333333333",
11
+ "reviewChatId": "-1004444444444",
12
+ "apiReviewRequired": true,
13
+ "chatReviewRequired": false
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,64 @@
1
+ # TelePost 投稿审核规则模板
2
+
3
+ > 这是管理员可直接修改的审核规则。AI Agent 只应依据本文件给出建议;本文件没有明确规定时,选择 `needs_human_review`,不要自行制定频道价值标准。
4
+
5
+ ## 频道主题
6
+
7
+ - 在此填写频道允许的主题范围。
8
+ - 在此填写明显偏离主题的内容类型。
9
+
10
+ ## 允许内容
11
+
12
+ - 例:与频道主题相关的原创或授权转载作品。
13
+ - 例:标题、标签、来源链接信息完整。
14
+
15
+ ## 不允许内容
16
+
17
+ - 违法、侵权或无法证明授权的内容。
18
+ - 恶意软件、钓鱼、引流欺诈、仇恨或骚扰内容。
19
+ - 与频道主题无关的广告或推广。
20
+
21
+ ## 广告与引流
22
+
23
+ - 说明哪些外链、水印、社群推广、个人主页链接可以接受。
24
+ - 说明哪些形式必须拒绝或要求人工复核。
25
+
26
+ ## 转载与版权
27
+
28
+ - 接受转载的条件:作者授权、来源许可、公共领域或合理使用证据。
29
+ - 缺少来源、作者或许可证据时的处理方式。
30
+
31
+ ## 图片与媒体质量
32
+
33
+ - 最低清晰度、裁切、水印、压缩、重复图片要求。
34
+ - 视频/GIF 的清晰度、时长、封面和声音要求。
35
+
36
+ ## 重复投稿
37
+
38
+ - 同一作品、同一来源链接或同一 idempotency key 的处理方式。
39
+ - 已发布、已拒绝、换图重投时的处理方式。
40
+
41
+ ## 标签、标题与备注
42
+
43
+ - 标签数量、语言、命名规范。
44
+ - 标题和备注允许包含的信息;禁止 Telegram/HTML 注入式格式。
45
+
46
+ ## NSFW / Spoiler
47
+
48
+ - 哪些内容必须开启 spoiler。
49
+ - 哪些内容必须拒绝,而不是仅开启 spoiler。
50
+ - 边界内容默认转人工。
51
+
52
+ ## 拒绝原因规范
53
+
54
+ - 用简短、具体、不带侮辱性的语言说明原因。
55
+ - 不暴露内部 token、用户隐私或系统细节。
56
+
57
+ ## 边界情况
58
+
59
+ 以下情况默认 `needs_human_review`:
60
+
61
+ - 政策没有明确规定。
62
+ - 版权、年龄限制、广告属性或真实性无法判断。
63
+ - 媒体预览不完整、视频封面无法代表内容。
64
+ - Agent 置信度不足或不同规则相互冲突。
config/settings.py ADDED
@@ -0,0 +1,425 @@
1
+ """
2
+ 配置文件读取和变量定义模块
3
+ """
4
+ import os
5
+ import sys
6
+ import configparser
7
+ import logging
8
+
9
+ from utils.run_mode import resolve_run_mode
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # 项目根目录:源码运行取仓库根;PyInstaller 冻结时取可执行文件所在目录,
14
+ # 保证 config.ini / data / logs 与 exe 同处一隅(双击即用)。
15
+ if getattr(sys, "frozen", False):
16
+ BASE_DIR = os.path.dirname(sys.executable)
17
+ else:
18
+ # Installed (pip) layout: config.ini ships with the package but the live
19
+ # config lives next to the user's working directory, not inside
20
+ # site-packages. Use cwd as the anchor so ``telepost --setup`` and the
21
+ # running bot read/write the same config.ini regardless of where the
22
+ # package is installed (source checkouts run from the repo root, so this
23
+ # is behaviour-preserving there too).
24
+ BASE_DIR = os.getcwd()
25
+ CONFIG_PATH = os.path.join(BASE_DIR, 'config.ini')
26
+
27
+ # 读取配置文件
28
+ config = configparser.ConfigParser()
29
+
30
+ # 安全读取配置文件
31
+ if os.path.exists(CONFIG_PATH):
32
+ config.read(CONFIG_PATH)
33
+ logger.info(f"已加载配置文件: {CONFIG_PATH}")
34
+ else:
35
+ logger.warning(f"⚠️ 配置文件 {CONFIG_PATH} 不存在,将仅使用环境变量")
36
+
37
+ # 辅助函数:安全获取配置
38
+ def get_config(section, key, fallback=None):
39
+ """安全获取配置值"""
40
+ try:
41
+ return config.get(section, key)
42
+ except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
43
+ return fallback
44
+
45
+ def get_config_int(section, key, fallback=0):
46
+ """安全获取整数配置值"""
47
+ try:
48
+ return config.getint(section, key)
49
+ except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
50
+ return fallback
51
+
52
+ def get_config_bool(section, key, fallback=False):
53
+ """安全获取布尔配置值"""
54
+ try:
55
+ return config.getboolean(section, key)
56
+ except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
57
+ return fallback
58
+
59
+ # 辅助函数:优先从环境变量获取,如果不存在则从配置文件获取
60
+ def get_env_or_config(env_key, section, config_key, fallback=None):
61
+ """
62
+ 优先从环境变量获取配置,如果环境变量不存在则从配置文件获取
63
+
64
+ 环境变量优先级规则:
65
+ - 如果环境变量存在(即使值为空字符串),使用环境变量的值
66
+ - 如果环境变量不存在,从配置文件读取
67
+ - 如果配置文件也不存在,使用 fallback 默认值
68
+
69
+ Args:
70
+ env_key: 环境变量名
71
+ section: 配置文件节名
72
+ config_key: 配置文件键名
73
+ fallback: 如果都不存在时的默认值
74
+
75
+ Returns:
76
+ 配置值(可能是字符串、None 或 fallback)
77
+ """
78
+ if env_key in os.environ:
79
+ # 环境变量存在,优先使用(即使值为空字符串,也使用环境变量的值)
80
+ value = os.environ[env_key]
81
+ logger.debug(f"使用环境变量 {env_key}={value[:20] + '...' if value and len(value) > 20 else (value if value else '(空)')}")
82
+ return value
83
+ else:
84
+ # 环境变量不存在,使用配置文件
85
+ value = get_config(section, config_key, fallback)
86
+ if value:
87
+ logger.debug(f"使用配置文件 {section}.{config_key}={value[:20] + '...' if len(value) > 20 else value}")
88
+ return value
89
+
90
+ # 从环境变量或配置文件获取配置(环境变量优先)
91
+ # Token 支持多个环境变量名:TOKEN(本项目约定)、BOT_TOKEN、TELEGRAM_BOT_TOKEN(社区惯用)。
92
+ # 注意:Fly.io 部署指南(fly.toml 注释 / deploy_flyio.sh / 文档)使用 BOT_TOKEN,
93
+ # 若只读 TOKEN 会导致按文档配置的部署直接启动失败,故此处必须做别名兼容。
94
+ TOKEN = (
95
+ get_env_or_config('TOKEN', 'BOT', 'TOKEN')
96
+ or os.getenv('BOT_TOKEN')
97
+ or os.getenv('TELEGRAM_BOT_TOKEN')
98
+ )
99
+ CHANNEL_ID = (
100
+ get_env_or_config('CHANNEL_ID', 'BOT', 'CHANNEL_ID')
101
+ or os.getenv('CHANNEL')
102
+ )
103
+ DB_PATH = get_env_or_config('DB_PATH', 'BOT', 'DB_PATH', fallback='data/submissions.db')
104
+ TIMEOUT = int(get_env_or_config('TIMEOUT', 'BOT', 'TIMEOUT') or get_config_int('BOT', 'TIMEOUT', 300))
105
+ ALLOWED_TAGS = int(get_env_or_config('ALLOWED_TAGS', 'BOT', 'ALLOWED_TAGS') or get_config_int('BOT', 'ALLOWED_TAGS', 30))
106
+ NET_TIMEOUT = 120 # 网络请求超时时间(秒)
107
+
108
+ # OWNER_ID 需要转换为整数类型
109
+ _owner_id_str = get_env_or_config('OWNER_ID', 'BOT', 'OWNER_ID')
110
+ try:
111
+ OWNER_ID = int(_owner_id_str) if _owner_id_str else None
112
+ except (ValueError, TypeError):
113
+ OWNER_ID = None
114
+ logger.warning(f"OWNER_ID 配置无效,无法转换为整数: {_owner_id_str}")
115
+
116
+ # ADMIN_IDS 管理员ID列表(用于管理命令)
117
+ _admin_ids_str = get_env_or_config('ADMIN_IDS', 'BOT', 'ADMIN_IDS') or ''
118
+ ADMIN_IDS = []
119
+ if _admin_ids_str:
120
+ try:
121
+ # 支持逗号分隔的多个ID
122
+ ADMIN_IDS = [int(id.strip()) for id in _admin_ids_str.split(',') if id.strip()]
123
+ except (ValueError, TypeError):
124
+ logger.warning(f"ADMIN_IDS 配置无效: {_admin_ids_str}")
125
+ ADMIN_IDS = []
126
+
127
+ # 如果设置了 OWNER_ID 且不在 ADMIN_IDS 中,自动添加
128
+ if OWNER_ID and OWNER_ID not in ADMIN_IDS:
129
+ ADMIN_IDS.append(OWNER_ID)
130
+
131
+ # 布尔值配置:环境变量优先
132
+ _show_submitter_env = os.getenv('SHOW_SUBMITTER')
133
+ if _show_submitter_env is not None:
134
+ SHOW_SUBMITTER = _show_submitter_env.lower() in ('true', '1', 'yes')
135
+ else:
136
+ SHOW_SUBMITTER = get_config_bool('BOT', 'SHOW_SUBMITTER', True)
137
+
138
+ _notify_owner_env = os.getenv('NOTIFY_OWNER')
139
+ if _notify_owner_env is not None:
140
+ NOTIFY_OWNER = _notify_owner_env.lower() in ('true', '1', 'yes')
141
+ else:
142
+ NOTIFY_OWNER = get_config_bool('BOT', 'NOTIFY_OWNER', True)
143
+
144
+ # ---- 投稿者发布通知策略(§notify-submitter)---------------------------------
145
+ # Publication success 是唯一触发点(review approval 不是):所有投稿路径
146
+ # (Chat DIRECT_PUBLISH / REVIEW_REQUIRED / EDITORIAL)共用同一条 durable
147
+ # 通知 pipeline。
148
+ # off 不通知
149
+ # published 只通知“已发布”
150
+ # with_changes 另外附带 editorial change summary(无编辑时不加)
151
+ SUBMITTER_PUBLISH_NOTIFY = os.getenv(
152
+ "SUBMITTER_PUBLISH_NOTIFY", get_config('BOT', 'SUBMITTER_PUBLISH_NOTIFY', 'off')
153
+ ).strip().lower() or 'off'
154
+ if SUBMITTER_PUBLISH_NOTIFY not in ("off", "published", "with_changes"):
155
+ SUBMITTER_PUBLISH_NOTIFY = 'off'
156
+
157
+
158
+ BOT_MODE = get_env_or_config('BOT_MODE', 'BOT', 'BOT_MODE', fallback='MIXED')
159
+
160
+ # 允许的文件类型配置
161
+ ALLOWED_FILE_TYPES = get_env_or_config('ALLOWED_FILE_TYPES', 'BOT', 'ALLOWED_FILE_TYPES', fallback='*')
162
+
163
+ # Webhook 与运行模式配置。AUTO 在存在有效公网 HTTPS URL 时选择 Webhook,
164
+ # 否则使用 Polling;run.py 多 bot 启动器会通过 RUN_MODE_REQUESTED 保留原始选择。
165
+ WEBHOOK_URL = get_env_or_config('WEBHOOK_URL', 'WEBHOOK', 'URL', fallback='')
166
+ _webhook_port = get_env_or_config('WEBHOOK_PORT', 'WEBHOOK', 'PORT')
167
+ WEBHOOK_PORT = int(_webhook_port) if _webhook_port else get_config_int('WEBHOOK', 'PORT', 8080)
168
+ WEBHOOK_PATH = get_env_or_config('WEBHOOK_PATH', 'WEBHOOK', 'PATH', fallback='/webhook')
169
+ WEBHOOK_SECRET_TOKEN = get_env_or_config('WEBHOOK_SECRET_TOKEN', 'WEBHOOK', 'SECRET_TOKEN', fallback='')
170
+
171
+ _run_mode = os.getenv('RUN_MODE_REQUESTED') or get_env_or_config(
172
+ 'RUN_MODE', 'BOT', 'RUN_MODE', fallback='AUTO'
173
+ )
174
+ RUN_MODE_REQUESTED = (_run_mode or 'AUTO').strip().upper()
175
+ RUN_MODE = resolve_run_mode(RUN_MODE_REQUESTED, WEBHOOK_URL)
176
+
177
+ # 搜索引擎配置
178
+ SEARCH_INDEX_DIR = get_env_or_config('SEARCH_INDEX_DIR', 'SEARCH', 'INDEX_DIR', fallback='data/search_index')
179
+ _search_enabled_env = os.getenv('SEARCH_ENABLED')
180
+ if _search_enabled_env is not None:
181
+ SEARCH_ENABLED = _search_enabled_env.lower() in ('true', '1', 'yes')
182
+ else:
183
+ SEARCH_ENABLED = get_config_bool('SEARCH', 'ENABLED', True)
184
+ SEARCH_ANALYZER = (get_env_or_config('SEARCH_ANALYZER', 'SEARCH', 'ANALYZER', fallback='jieba') or 'jieba').strip().lower()
185
+ _search_highlight_env = os.getenv('SEARCH_HIGHLIGHT')
186
+ if _search_highlight_env is not None:
187
+ SEARCH_HIGHLIGHT = _search_highlight_env.lower() in ('true', '1', 'yes')
188
+ else:
189
+ SEARCH_HIGHLIGHT = get_config_bool('SEARCH', 'HIGHLIGHT', False)
190
+
191
+ # 数据库配置
192
+ _db_cache_kb = get_env_or_config('DB_CACHE_KB', 'DB', 'CACHE_SIZE_KB')
193
+ DB_CACHE_KB = int(_db_cache_kb) if _db_cache_kb else get_config_int('DB', 'CACHE_SIZE_KB', 4096) # SQLite page cache,单位KB
194
+
195
+ # 单条投稿可包含的文件数;发布时会自动拆成 Telegram 每组 10 个相册。
196
+ try:
197
+ MAX_SUBMISSION_FILES = max(1, int(get_env_or_config('API_MAX_FILES', 'BOT', 'API_MAX_FILES', fallback='100')))
198
+ except (ValueError, TypeError):
199
+ MAX_SUBMISSION_FILES = 100
200
+
201
+ # 投稿频率限制:每用户每小时最多发起投稿次数,0 为关闭
202
+ _submit_limit = get_env_or_config('SUBMIT_LIMIT_PER_HOUR', 'BOT', 'SUBMIT_LIMIT_PER_HOUR')
203
+ try:
204
+ SUBMIT_LIMIT_PER_HOUR = int(_submit_limit) if _submit_limit else get_config_int('BOT', 'SUBMIT_LIMIT_PER_HOUR', 10)
205
+ except (ValueError, TypeError):
206
+ SUBMIT_LIMIT_PER_HOUR = 10
207
+
208
+ # 投稿审核来源开关。两个开关相互独立,可只审核 API、只审核聊天,或同时审核。
209
+ _api_review_required = get_env_or_config(
210
+ 'API_REVIEW_REQUIRED', 'BOT', 'API_REVIEW_REQUIRED', fallback='true'
211
+ )
212
+ API_REVIEW_REQUIRED = str(_api_review_required).lower() in ('true', '1', 'yes')
213
+
214
+ _miniapp_review_required = get_env_or_config(
215
+ 'MINIAPP_REVIEW_REQUIRED', 'BOT', 'MINIAPP_REVIEW_REQUIRED', fallback='true'
216
+ )
217
+ MINIAPP_REVIEW_REQUIRED = str(_miniapp_review_required).lower() in ('true', '1', 'yes')
218
+
219
+ _chat_review_required = get_env_or_config(
220
+ 'CHAT_REVIEW_REQUIRED', 'BOT', 'CHAT_REVIEW_REQUIRED', fallback='false'
221
+ )
222
+ CHAT_REVIEW_REQUIRED = str(_chat_review_required).lower() in ('true', '1', 'yes')
223
+
224
+ _review_chat_id = get_env_or_config('REVIEW_CHAT_ID', 'BOT', 'REVIEW_CHAT_ID', fallback='')
225
+ if _review_chat_id:
226
+ try:
227
+ REVIEW_CHAT_ID = int(_review_chat_id)
228
+ except (ValueError, TypeError):
229
+ REVIEW_CHAT_ID = str(_review_chat_id).strip()
230
+ else:
231
+ REVIEW_CHAT_ID = None
232
+
233
+ # 验证必要配置
234
+ if not TOKEN:
235
+ raise ValueError("❌ TOKEN 未设置!请在环境变量或 config.ini 中设置")
236
+ if not CHANNEL_ID:
237
+ raise ValueError("❌ CHANNEL_ID 未设置!请在环境变量或 config.ini 中设置")
238
+ if (API_REVIEW_REQUIRED or MINIAPP_REVIEW_REQUIRED or CHAT_REVIEW_REQUIRED) and not REVIEW_CHAT_ID:
239
+ raise ValueError(
240
+ "❌ 已开启 MINIAPP_REVIEW_REQUIRED / API_REVIEW_REQUIRED / "
241
+ "CHAT_REVIEW_REQUIRED,但 REVIEW_CHAT_ID 未设置"
242
+ )
243
+
244
+ # 审核群绝不能与投稿频道是同一个会话:否则审核预览相册、控制消息和
245
+ # PixivFlow 空结果/失败通知都会以"回复/散帖"形式出现在频道里,
246
+ # 造成用户看到的"主贴后面莫名跟着几条奇怪回复"。
247
+ if REVIEW_CHAT_ID is not None:
248
+ _chan = str(CHANNEL_ID).strip()
249
+ _review = str(REVIEW_CHAT_ID).strip()
250
+ if _chan == _review:
251
+ raise ValueError(
252
+ "❌ REVIEW_CHAT_ID 不能等于 CHANNEL_ID:审核群与投稿频道必须是"
253
+ "两个不同的会话,否则预览/通知会混进频道"
254
+ )
255
+ if _chan.lstrip('-').isdigit() and _review.lstrip('-').isdigit():
256
+ try:
257
+ if int(_chan) == int(_review):
258
+ raise ValueError(
259
+ "❌ REVIEW_CHAT_ID 不能等于 CHANNEL_ID:审核群与投稿频道"
260
+ "必须是两个不同的会话,否则预览/通知会混进频道"
261
+ )
262
+ except ValueError:
263
+ pass
264
+
265
+ # 频道发布 footer(§submission-entrypoint):正式发布到频道的帖子会在 caption
266
+ # 最下方追加文本导航 footer:
267
+ # ✉️ TG 投稿 → https://t.me/<bot>?start=submit
268
+ # 📱 Mini App → https://t.me/<bot>?startapp=miniapp(MINIAPP_SUBMIT_CTA=true 时;
269
+ # 配置 Direct Mini App short name 后才直接 ?startapp=submit)
270
+ # 标签是固定展示契约,不读取 CHANNEL_FOOTER_TEXT(保留仅为兼容旧部署)。
271
+ CHANNEL_FOOTER_LINK = (
272
+ get_env_or_config('CHANNEL_FOOTER_LINK', 'BOT', 'CHANNEL_FOOTER_LINK',
273
+ fallback='')
274
+ or ''
275
+ ).strip()
276
+ CHANNEL_FOOTER_TEXT = (
277
+ get_env_or_config('CHANNEL_FOOTER_TEXT', 'BOT', 'CHANNEL_FOOTER_TEXT',
278
+ fallback='点击投稿')
279
+ or '点击投稿'
280
+ ).strip()
281
+
282
+ # 频道 footer 额外追加 Mini App 导航项。startapp 只是导航意图,身份仍由
283
+ # 服务器校验的 Telegram initData 决定。未启用或链接缺失时省略该导航项,
284
+ # 绝不为缺失配置生成坏链接。
285
+ MINIAPP_SUBMIT_CTA = str(
286
+ get_env_or_config('MINIAPP_SUBMIT_CTA', 'BOT', 'MINIAPP_SUBMIT_CTA',
287
+ fallback='false')
288
+ or 'false'
289
+ ).strip().lower() in {'1', 'true', 'yes', 'on'}
290
+
291
+ # Mini App surface gate. Enabled by default (matches the Mini App session/auth
292
+ # gate in telepost/miniapp/auth.init_data_enabled); set explicitly to
293
+ # 'false'/'0'/'no'/'off' to disable the private-chat Web App buttons. It never
294
+ # affects chat/API publishing. A disabled-by-absence split previously made the
295
+ # chat-side capability report "Mini App not enabled" while the session endpoint
296
+ # stayed enabled, so Bot-side capability and actual Mini App availability
297
+ # disagreed; both now share one default-on-unless-explicitly-disabled contract.
298
+ _miniapp_enabled_raw = str(
299
+ get_env_or_config('MINIAPP_ENABLED', 'BOT', 'MINIAPP_ENABLED', fallback='true')
300
+ or 'true'
301
+ ).strip().lower()
302
+ MINIAPP_ENABLED = _miniapp_enabled_raw not in {'false', '0', 'no', 'off'}
303
+
304
+ # Mini App optional content browsing (Hot / post detail / post media). It is
305
+ # independent from the core submission pipeline and can be disabled without
306
+ # affecting Bot submission, review or publication.
307
+ MINIAPP_CONTENT_ENABLED = str(
308
+ get_env_or_config('MINIAPP_CONTENT_ENABLED', 'BOT', 'MINIAPP_CONTENT_ENABLED',
309
+ fallback='true')
310
+ or 'true'
311
+ ).strip().lower() in {'1', 'true', 'yes', 'on'}
312
+
313
+ # Optional BotFather Direct Mini App short name. When set, channel footers can
314
+ # open the attached app directly. Without it, navigation falls back to
315
+ # ?startapp=miniapp, which opens the Main Mini App directly.
316
+ MINIAPP_SHORT_NAME = (
317
+ get_env_or_config('MINIAPP_SHORT_NAME', 'BOT', 'MINIAPP_SHORT_NAME',
318
+ fallback='')
319
+ or ''
320
+ ).strip().lstrip('/')
321
+
322
+ # Public Web App URL used by keyboard buttons. Deployment normally serves the
323
+ # app at the same domain as the webhook; the fallback avoids repeating the URL.
324
+ MINIAPP_PUBLIC_URL = (
325
+ get_env_or_config('MINIAPP_PUBLIC_URL', 'BOT', 'MINIAPP_PUBLIC_URL',
326
+ fallback='')
327
+ or (os.getenv('WEBHOOK_URL', '').rstrip('/') + '/app/' if os.getenv('WEBHOOK_URL') else '')
328
+ ).strip() or ''
329
+ # 可选发布增强:TXT 小说通过 TelePress 发布到 Telegraph 提供「在线阅读」
330
+ # (§telepress-preview)。它是可选 enrichment:默认关闭;开启后仍由 Telegram
331
+ # TXT document 决定 Publication 成功与否,Telegraph 失败/超时绝不回滚或失败发布。
332
+ # TELEGRAPH_ACCESS_TOKEN 是 Telegraph 账户 access token(可创建匿名账户获得),
333
+ # 仅作为 Secret 注入,绝不写日志。
334
+ _novel_preview_enabled = get_env_or_config(
335
+ 'NOVEL_PREVIEW_ENABLED', 'NOVEL_PREVIEW', 'ENABLED', fallback='false'
336
+ )
337
+ NOVEL_PREVIEW_ENABLED = str(_novel_preview_enabled).lower() in ('true', '1', 'yes')
338
+
339
+ # 无真实封面的小说在频道需要一个轻量 fallback 卡片作为 root(§novel-cover)。
340
+ # TelePost 展示层职责:Pillow 现场生成、可关闭;生成失败降级为 text-only root,
341
+ # 绝不影响 Publication 成败。默认开启。
342
+ NOVEL_FALLBACK_CARD_ENABLED = str(
343
+ get_env_or_config('NOVEL_FALLBACK_CARD_ENABLED', 'BOT', 'NOVEL_FALLBACK_CARD_ENABLED',
344
+ fallback='true')
345
+ or 'true'
346
+ ).strip().lower() in {'1', 'true', 'yes', 'on'}
347
+
348
+ _novel_preview_timeout = get_env_or_config(
349
+ 'NOVEL_PREVIEW_TIMEOUT_SECONDS', 'NOVEL_PREVIEW', 'TIMEOUT_SECONDS',
350
+ fallback='15'
351
+ )
352
+ try:
353
+ NOVEL_PREVIEW_TIMEOUT_SECONDS = max(1.0, float(_novel_preview_timeout))
354
+ except (ValueError, TypeError):
355
+ NOVEL_PREVIEW_TIMEOUT_SECONDS = 15.0
356
+
357
+ _novel_preview_max_bytes = get_env_or_config(
358
+ 'NOVEL_PREVIEW_MAX_BYTES', 'NOVEL_PREVIEW', 'MAX_BYTES', fallback='4194304'
359
+ )
360
+ try:
361
+ NOVEL_PREVIEW_MAX_BYTES = max(1024, int(_novel_preview_max_bytes))
362
+ except (ValueError, TypeError):
363
+ NOVEL_PREVIEW_MAX_BYTES = 4 * 1024 * 1024
364
+
365
+ TELEGRAPH_ACCESS_TOKEN = (
366
+ os.getenv('TELEGRAPH_ACCESS_TOKEN', '')
367
+ or get_config('NOVEL_PREVIEW', 'TELEGRAPH_ACCESS_TOKEN', '')
368
+ or ''
369
+ ).strip()
370
+
371
+ # 模式常量定义
372
+ MODE_MEDIA = 'MEDIA' # 仅媒体上传
373
+ MODE_DOCUMENT = 'DOCUMENT' # 仅文档上传
374
+ MODE_MIXED = 'MIXED' # 混合模式
375
+
376
+ # 打印配置信息(调试用)
377
+ logger.info(f"配置加载完成:")
378
+ logger.info(f" - BOT_MODE: {BOT_MODE}")
379
+ logger.info(
380
+ f" - RUN_MODE: {RUN_MODE_REQUESTED} -> {RUN_MODE}"
381
+ if RUN_MODE_REQUESTED == 'AUTO'
382
+ else f" - RUN_MODE: {RUN_MODE}"
383
+ )
384
+ logger.info(f" - CHANNEL_ID: {CHANNEL_ID}")
385
+ logger.info(f" - DB_PATH: {DB_PATH}")
386
+ logger.info(f" - TIMEOUT: {TIMEOUT}")
387
+ logger.info(f" - OWNER_ID: {OWNER_ID if OWNER_ID else '未设置'}")
388
+ logger.info(f" - ADMIN_IDS: {ADMIN_IDS if ADMIN_IDS else '未设置'}")
389
+ logger.info(f" - API_REVIEW_REQUIRED: {API_REVIEW_REQUIRED}")
390
+ logger.info(f" - MINIAPP_REVIEW_REQUIRED: {MINIAPP_REVIEW_REQUIRED}")
391
+ logger.info(f" - CHAT_REVIEW_REQUIRED: {CHAT_REVIEW_REQUIRED}")
392
+ logger.info(f" - REVIEW_CHAT_ID: {REVIEW_CHAT_ID if REVIEW_CHAT_ID else '未设置'}")
393
+ logger.info(f" - CHANNEL_FOOTER_LINK: {CHANNEL_FOOTER_LINK if CHANNEL_FOOTER_LINK else '未设置(不追加 footer)'}")
394
+ logger.info(f" - ALLOWED_FILE_TYPES: {ALLOWED_FILE_TYPES}")
395
+ if RUN_MODE != "WEBHOOK":
396
+ try:
397
+ from handlers.publish import CHANNEL_ALBUM_REPLY as _album_reply
398
+ except Exception:
399
+ _album_reply = os.getenv("CHANNEL_ALBUM_REPLY", "chain").strip().lower()
400
+ if _album_reply == "discussion":
401
+ logger.warning(
402
+ " - ⚠️ CHANNEL_ALBUM_REPLY=discussion 仅在 Webhook 模式可用:"
403
+ "自动转发事件进不了 Polling 进程,多图发布会在等待转发超时后失败。"
404
+ "请设置 RUN_MODE=WEBHOOK,或改回 chain/post。"
405
+ )
406
+ if RUN_MODE == 'WEBHOOK':
407
+ logger.info(f" - WEBHOOK_URL: {WEBHOOK_URL if WEBHOOK_URL else '未设置'}")
408
+ logger.info(f" - WEBHOOK_PORT: {WEBHOOK_PORT}")
409
+ logger.info(f" - WEBHOOK_PATH: {WEBHOOK_PATH}")
410
+ logger.info(f" - WEBHOOK_SECRET: {'已设置' if WEBHOOK_SECRET_TOKEN else '未设置(将自动生成)'}")
411
+ logger.info(f" - SEARCH_INDEX_DIR: {SEARCH_INDEX_DIR}")
412
+ logger.info(f" - SEARCH_ENABLED: {SEARCH_ENABLED}")
413
+ logger.info(f" - SEARCH_ANALYZER: {SEARCH_ANALYZER}")
414
+ logger.info(f" - SEARCH_HIGHLIGHT: {SEARCH_HIGHLIGHT}")
415
+ logger.info(f" - DB_CACHE_KB: {DB_CACHE_KB}")
416
+
417
+ # Public fixed-upstream media proxy for remote delivery sources. Empty keeps the
418
+ # existing behavior: Telegram receives the canonical source URL unchanged.
419
+ MEDIA_PROXY_BASE_URL = os.getenv("MEDIA_PROXY_BASE_URL", "").strip().rstrip("/")
420
+ _MEDIA_PROXY_HOSTS_RAW = os.getenv("MEDIA_PROXY_HOSTS", "").strip()
421
+ MEDIA_PROXY_HOSTS = frozenset(
422
+ host.strip().lower()
423
+ for host in _MEDIA_PROXY_HOSTS_RAW.split(",")
424
+ if host.strip()
425
+ )
database/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ 数据库模块
3
+ """