main
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3import asyncio
4import os
5from pathlib import Path
6
7from cacheout import Cache
8from cutword import Cutter
9
10# init some global instances
11cache = Cache(ttl=0, maxsize=2048)
12semaphore = asyncio.Semaphore(8) # max 8 concurrent downloads
13cutter = Cutter()
14
15DOWNLOAD_DIR = os.getenv("DOWNLOAD_DIR", Path(__file__).parent.joinpath("downloads").as_posix())
16FONTS_DIR = os.getenv("FONTS_DIR", Path(DOWNLOAD_DIR).joinpath("fonts").as_posix())
17FILE_SERVER = os.getenv("FILE_SERVER", "") # expose the download dir to internet (optional). for example: https://server.com/dir
18TZ = os.getenv("TZ", "Asia/Shanghai")
19DEVICE_NAME = os.getenv("DEVICE_NAME", "BennyBot")
20REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "60")) # seconds
21TEXT_LENGTH = int(os.getenv("TEXT_LENGTH", "4096")) # Maximum length of text message
22CAPTION_LENGTH = int(os.getenv("CAPTION_LENGTH", "1024")) # 4096 for Premium user
23MAX_FILE_BYTES = int(os.getenv("MAX_FILE_BYTES", "2000")) * 1024 * 1024 # 4000 MB for Premium user
24MAX_MESSAGE_RETRIEVED = int(os.getenv("MAX_MESSAGE_RETRIEVED", "1000000")) # Maximum number of messages to retrieve
25MAX_MESSAGE_SUMMARY = int(os.getenv("MAX_MESSAGE_SUMMARY", "9999")) # Maximum number of messages to summay
26READING_SPEED = int(os.getenv("READING_SPEED", "600")) # words per minute
27DAILY_MESSAGES = os.getenv("DAILY_MESSAGES", "{}") # Useful for daily checkin for some services. Should be a json string: '{"chat-1": "msg-1", "chat-2": "msg-2"}'
28# For ytdlp downloaded video, re-encoding to H264 format. This set the max file size for re-encoding. Default: 1PB
29YTDLP_RE_ENCODING_MAX_FILE_BYTES = int(os.getenv("YTDLP_RE_ENCODING_MAX_FILE_BYTES", "1125899906842624"))
30# ytdlp max allowed file bytes. Default: 1PB (Set this if the VPS disk space is limited)
31YTDLP_DOWNLOAD_MAX_FILE_BYTES = int(os.getenv("YTDLP_DOWNLOAD_MAX_FILE_BYTES", "1125899906842624"))
32TELEGRAM_UA = os.getenv("TELEGRAM_UA", "TelegramBot (like TwitterBot)")
33NUM_YOUTUBE_SEARCH_RESULTS = int(os.getenv("NUM_YOUTUBE_SEARCH_RESULTS", "10")) # Number of youtube search results
34NUM_GOOGLE_SEARCH_RESULTS = int(os.getenv("NUM_GOOGLE_SEARCH_RESULTS", "10")) # Number of google search results
35GOOGLE_SEARCH_GL = os.getenv("GOOGLE_SEARCH_GL", "cn") # "gl" parameter (Geolocation)
36CLEAN_OLD_FILES_OLDER_THAN_SECONDS = int(os.getenv("CLEAN_OLD_FILES_OLDER_THAN_SECONDS", "7200"))
37
38
39class ENABLE: # see fine-grained permission in `src/permission.py`
40 AI = os.getenv("ENABLE_AI", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
41 ASR = os.getenv("ENABLE_ASR", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
42 AUDIO = os.getenv("ENABLE_AUDIO", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
43 CRONTAB = os.getenv("ENABLE_CRONTAB", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
44 DOUYIN = os.getenv("ENABLE_DOUYIN", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
45 INSTAGRAM = os.getenv("ENABLE_INSTAGRAM", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
46 OCR = os.getenv("ENABLE_OCR", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
47 HISTORY = os.getenv("ENABLE_HISTORY", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
48 PRICE = os.getenv("ENABLE_PRICE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
49 SEARCH_YOUTUBE = os.getenv("ENABLE_SEARCH_YOUTUBE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
50 SEARCH_GOOGLE = os.getenv("ENABLE_SEARCH_GOOGLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
51 SUBTITLE = os.getenv("ENABLE_SUBTITLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
52 TIKTOK = os.getenv("ENABLE_TIKTOK", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
53 TWITTER = os.getenv("ENABLE_TWITTER", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
54 WEIBO = os.getenv("ENABLE_WEIBO", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
55 WECHAT = os.getenv("ENABLE_WECHAT", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
56 REDDIT = os.getenv("ENABLE_REDDIT", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
57 V2EX = os.getenv("ENABLE_V2EX", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
58 ARXIV = os.getenv("ENABLE_ARXIV", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
59 WGET = os.getenv("ENABLE_WGET", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
60 GITHUB = os.getenv("ENABLE_GITHUB", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
61 MUSIC163 = os.getenv("ENABLE_MUSIC163", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
62 SPOTIFY = os.getenv("ENABLE_SPOTIFY", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
63 XHS = os.getenv("ENABLE_XHS", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
64 YTDLP = os.getenv("ENABLE_YTDLP", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
65 YTDLP_BILIBILI = os.getenv("ENABLE_YTDLP_BILIBILI", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
66 YTDLP_YOUTUBE = os.getenv("ENABLE_YTDLP_YOUTUBE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
67 RAW_IMG_CONVERT = os.getenv("ENABLE_RAW_IMG_CONVERT", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
68 GROUPS = os.getenv("ENABLE_GROUPS", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
69 CHANNELS = os.getenv("ENABLE_CHANNELS", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
70 BOTS = os.getenv("ENABLE_BOTS", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
71 USERS = os.getenv("ENABLE_USERS", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
72 SEND_AS_REPLY = os.getenv("ENABLE_SEND_AS_REPLY", "1").lower() in ["1", "y", "yes", "t", "true", "on"] # Send as a reply to the original message
73 CACHE_PRICE_SYMBOLS = os.getenv("ENABLE_CACHE_PRICE_SYMBOLS", "0").lower() in ["1", "y", "yes", "t", "true", "on"]
74 QUERY_DANMU = os.getenv("ENABLE_QUERY_DANMU", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
75 FAVORITE = os.getenv("ENABLE_FAVORITE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
76 TTS = os.getenv("ENABLE_TTS", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
77 CONVERT_CHINESE = os.getenv("ENABLE_CONVERT_CHINESE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
78 QUOTLY = os.getenv("ENABLE_QUOTLY", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
79 TMDB = os.getenv("ENABLE_TMDB", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
80 FFMPEG = os.getenv("ENABLE_FFMPEG", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
81 WATERMARK = os.getenv("ENABLE_WATERMARK", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
82 VERSION = os.getenv("ENABLE_VERSION", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
83
84
85class PREFIX:
86 SOCIAL_MEDIA = os.getenv("PREFIX_SOCIAL_MEDIA", "/benny, /dl, !dl")
87 AI_SUMMARY = os.getenv("PREFIX_AI_SUMMARY", "/summary").lower()
88 AI_TEXT_GENERATION = os.getenv("PREFIX_AI_TEXT_GENERATION", "/ai").lower()
89 AI_IMG_GENERATION = os.getenv("PREFIX_AI_IMG_GENERATION", "/img").lower()
90 AI_VIDEO_GENERATION = os.getenv("PREFIX_AI_VIDEO_GENERATION", "/gvid").lower()
91 ASR = os.getenv("PREFIX_ASR", "/asr").lower()
92 AUDIO = os.getenv("PREFIX_AUDIO", "/audio").lower()
93 CONVERT = os.getenv("PREFIX_CONVERT", "/convert").lower() # convert image file to photo
94 SUBTITLE = os.getenv("PREFIX_SUBTITLE", "/subtitle, /sub").lower()
95 WGET = os.getenv("PREFIX_WGET", "/wget, /curl").lower()
96 OCR = os.getenv("PREFIX_OCR", "/ocr").lower()
97 PRICE = os.getenv("PREFIX_PRICE", "/price").lower() # unify crypto, stock
98 CRYPTO = os.getenv("PREFIX_CRYPTO", "/crypto").lower() # crypto only
99 STOCK = os.getenv("PREFIX_STOCK", "/stock").lower() # stock only
100 COMBINATION = os.getenv("PREFIX_COMBINATION", "/combine").lower()
101 VOICE = os.getenv("PREFIX_VOICE", "/voice").lower()
102 SEARCH_YOUTUBE = os.getenv("PREFIX_SEARCH_YOUTUBE", "/youtube, /ytb").lower()
103 SEARCH_GOOGLE = os.getenv("PREFIX_SEARCH_GOOGLE", "/google").lower()
104 DANMU = os.getenv("PREFIX_DANMU", "/danmu").lower()
105 FAYAN = os.getenv("PREFIX_FAYAN", "/fa").lower()
106 HISTORY = "/history, /hist"
107 TTS = os.getenv("PREFIX_TTS", "/tts").lower()
108 CONVERT_TO_TC = os.getenv("PREFIX_CONVERT_TO_TC", "/tc, /tw").lower()
109 CONVERT_TO_SC = os.getenv("PREFIX_CONVERT_TO_SC", "/sc, /cn").lower()
110 QUOTLY = os.getenv("PREFIX_QUOTLY", "/quote").lower()
111 TMDB = os.getenv("PREFIX_TMDB", "/tmdb").lower()
112 FFMPEG_CUT = os.getenv("PREFIX_FFMPEG_CUT", "/cut").lower()
113 FFMPEG_H264 = os.getenv("PREFIX_FFMPEG_H264", "/h264").lower()
114 FFPROBE = os.getenv("PREFIX_FFPROBE", "/ffprobe").lower()
115 WATERMARK = os.getenv("PREFIX_WATERMARK", "/wm, /watermark").lower()
116 VERSION = os.getenv("PREFIX_VERSION", "/version").lower()
117 MSG_INFO = os.getenv("PREFIX_MSG_INFO", "/info").lower()
118
119
120class API:
121 FXTWITTER = os.getenv("FXTWITTER_API", "https://api.fxtwitter.com")
122 VXTWITTER = os.getenv("VXTWITTER_API", "https://api.vxtwitter.com")
123 DDINSTAGRAM = os.getenv("DDINSTAGRAM_API", "https://www.ddinstagram.com")
124 TIKHUB = os.getenv("TIKHUB", "https://api.tikhub.io")
125 TIKHUB_FREE = os.getenv("TIKHUB_FREE", "https://api.douyin.wtf")
126 TIKHUB_INSTAGRAM = os.getenv("TIKHUB_INSTAGRAM_API", "https://api.tikhub.io/api/v1/instagram/v1/fetch_post_by_url?post_url=")
127 TIKHUB_TWITTER = os.getenv("TIKHUB_TWITTER_API", "https://api.tikhub.io/api/v1/twitter/web/fetch_post_comments?tweet_id=")
128 TIKHUB_WEIBO_VIDEO = os.getenv("TIKHUB_WEIBO_VIDEO_API", "https://api.tikhub.io/api/v1/weibo/web/fetch_short_video_data?share_text=")
129 TIKHUB_WECHAT = os.getenv("TIKHUB_WECHAT", "https://api.tikhub.io/api/v1/wechat_mp/web/fetch_mp_article_detail_json?url=")
130 BINANCE_SPOT = os.getenv("BINANCE_SPOT_API", "https://data-api.binance.vision")
131 BINANCE_UM = os.getenv("BINANCE_UM_API", "https://fapi.binance.com")
132 OKX = os.getenv("OKX_API", "https://www.okx.com")
133 QUOTELY = os.getenv("QUOTLY_API", "https://bot.lyo.su/quote/generate")
134
135
136class DANMU:
137 BASE_URL = os.getenv("DANMU_BASE_URL", "") # Custom API, No docs
138 STREAMER = os.getenv("DANMU_STREAMER", "Streamer") # streamer name
139 AUTH_USER = os.getenv("DANMU_AUTH_USER", "") # username for basic auth
140 AUTH_PASS = os.getenv("DANMU_AUTH_PASS", "") # password for basic auth
141 QUERY_METHOD = os.getenv("DANMU_QUERY_METHOD", "turso") # Turso or R2+API server
142 NUM_PER_QUERY = int(os.getenv("DANMU_NUM_PER_QUERY", "100")) # Number of items per query to API server
143 D1_DATABASE = os.getenv("DANMU_D1_DATABASE", "bennybot-danmu")
144 TURSO_DATABASE = os.getenv("DANMU_TURSO_DATABASE", "bennybot-danmu")
145 TURSO_USERNAME = os.getenv("DANMU_TURSO_USERNAME", "") # https://turso.tech
146 TURSO_API_TOKEN = os.getenv("DANMU_TURSO_API_TOKEN", "")
147 TURSO_GROUP_TOKEN = os.getenv("DANMU_TURSO_GROUP_TOKEN", "")
148 R2_PREFIX = os.getenv("DANMU_R2_PREFIX", "Streaming/")
149 SYNC_ENABLE = os.getenv("DANMU_SYNC_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
150 SYNC_ENGNIE = os.getenv("DANMU_SYNC_ENGNIE", "turso,R2") # sync livechats to Turso & R2
151 SYNC_DANMU_YEARS = os.getenv("SYNC_DANMU_YEARS", "") # comma separated years to sync. e.g. "2025,2024,2023"
152 SYNC_FAYAN_YEARS = os.getenv("SYNC_FAYAN_YEARS", "") # comma separated years that has live stream
153
154
155class PROVIDER: # default API provider
156 DOUYIN = os.getenv("DOUYIN_PROVIDER", "direct-free-tikhub-bridge").lower()
157 DOUYIN_COMMENTS = os.getenv("DOUYIN_COMMENTS_PROVIDER", "free-tikhub").lower() # a false value (0, false, none, null) to disable it
158 TWITTER = os.getenv("TWITTER_PROVIDER", "tikhub-vxtwitter-fxtwitter-bridge").lower()
159 INSTAGRAM = os.getenv("INSTAGRAM_PROVIDER", "tikhub-ddinstagram-bridge").lower()
160 WEIBO = os.getenv("WEIBO_PROVIDER", "direct-bridge").lower()
161 XHS = os.getenv("XHS_PROVIDER", "direct-bridge").lower()
162
163
164class TOKEN:
165 SESSION_STRING = os.getenv("SESSION_STRING", "")
166 TIKHUB = os.getenv("TIKHUB_TOKEN", "")
167 YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "")
168 CMC_API_KEY = os.getenv("CMC_API_KEY", "")
169 GOOGLE_SEARCH_API_KEY = os.getenv("GOOGLE_SEARCH_API_KEY", "")
170 GOOGLE_SEARCH_CX = os.getenv("GOOGLE_SEARCH_CX", "")
171 CHART_IMG = os.getenv("CHART_IMG_KEY", "")
172 TELEGRAPH = os.getenv("TELEGRAPH_TOKEN", "")
173 NEOCITIES = os.getenv("NEOCITIES_USERPASS", "") # in "user,pass" format
174 NEOCITIES_IV_HASH = os.getenv("NEOCITIES_INSTANTVIEW_HASH", "")
175 R2_IV_HASH = os.getenv("R2_INSTANTVIEW_HASH", "")
176 GITHUB = os.getenv("GITHUB_TOKEN", "")
177 SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID", "")
178 SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET", "")
179 V2EX = os.getenv("V2EX_TOKEN", "")
180 TMDB = os.getenv("TMDB_TOKEN", "")
181
182
183class PROXY: # format: socks5://127.0.0.1:7890
184 AI_POST = os.getenv("AI_POST_PROXY", None)
185 ALI = os.getenv("ALI_PROXY", None)
186 ANTHROPIC = os.getenv("ANTHROPIC_PROXY", None)
187 CLOUDFLARE = os.getenv("CLOUDFLARE_PROXY", None)
188 CRYPTO = os.getenv("CRYPTO_PROXY", None)
189 D1 = os.getenv("D1_PROXY", None)
190 DANMU = os.getenv("DANMU_PROXY", None)
191 DOUYIN = os.getenv("DOUYIN_PROXY", None)
192 DOWNLOAD = os.getenv("DOWNLOAD_PROXY", None)
193 EDGE = os.getenv("TTS_EDGE_PROXY", None)
194 GITHUB = os.getenv("GITHUB_PROXY", None)
195 GOOGLE = os.getenv("GOOGLE_PROXY", None)
196 GROQ = os.getenv("GROQ_PROXY", None) # Ban CN & HK IP
197 IMG = os.getenv("IMG_PROXY", "") # https://caravaggio.ramielcreations.com/docs/install
198 INSTAGRAM = os.getenv("INSTAGRAM_PROXY", None)
199 OPENAI = os.getenv("OPENAI_PROXY", None)
200 PODCAST = os.getenv("PODCAST_PROXY", None)
201 REDDIT = os.getenv("REDDIT_PROXY", None)
202 SPOTIFY = os.getenv("SPOTIFY_PROXY", None)
203 SUBTITLE = os.getenv("SUBTITLE_PROXY", None)
204 TELEGRAM = os.getenv("TELEGRAM_PROXY", None) # Telegram
205 TENCENT = os.getenv("TENCENT_PROXY", None) # Banned oversea IP, need a back to China proxy
206 TIKTOK = os.getenv("TIKTOK_PROXY", None)
207 TMDB = os.getenv("TMDB_PROXY", None)
208 TURSO = os.getenv("TURSO_PROXY", None)
209 TWITTER = os.getenv("TWITTER_PROXY", None)
210 V2EX = os.getenv("V2EX_PROXY", None)
211 WARP = os.getenv("WARP_PROXY", None)
212 WECHAT = os.getenv("WECHAT_PROXY", None)
213 WEIBO = os.getenv("WEIBO_PROXY", None)
214 ARXIV = os.getenv("ARXIV_PROXY", None)
215 XHS = os.getenv("XHS_PROXY", None) # Banned VPS IP, need residential proxy
216 YTDLP = os.getenv("YTDLP_PROXY", None) # general proxy for ytdlp
217 YTDLP_FALLBACK = os.getenv("YTDLP_PROXY_FALLBACK", None) # fallback proxy for ytdlp
218 # for ytdlp proxy of specific sites (Like Bilibili), use this format: YTDLP_PROXY_BILIBILI
219
220
221class COOKIE: # See: https://github.com/easychen/CookieCloud
222 CLOUD_SERVER = os.getenv("COOKIE_CLOUD_SERVER", "")
223 CLOUD_KEY = os.getenv("COOKIE_CLOUD_KEY", "")
224 CLOUD_PASS = os.getenv("COOKIE_CLOUD_PASS", "")
225 YTDLP_BILIBILI_USE_COOKIE = os.getenv("YTDLP_BILIBILI_USE_COOKIE", "0").lower() in ["1", "y", "yes", "t", "true", "on"]
226
227
228class TID: # see more TID usecase in `src/permission.py`
229 ADMIN = os.getenv("TID_ADMIN", "") # comma separated userid or @username
230 TEMP = os.getenv("TID_TEMP", "me") # a temperary chat for some tasks
231 HISTORY_ADMIN = os.getenv("TID_HISTORY_ADMIN", "") # comma separated userid (@username is NOT supported!)
232 # back up ytdlp audio if the user does not request it
233 DAILY_SUMMARY = os.getenv("TID_DAILY_SUMMARY", "{}") # {"source-chat-id": "target-chat-id"}, e.g. '{"-1001234567890": "-1009876543210"}'
234 GEMINI_CHATS = os.getenv("TID_GEMINI_CHATS", "") # comma separated chat ids to always use gemini models (no need `/gemini`)
235 OPENAI_CHATS = os.getenv("TID_OPENAI_CHATS", "") # comma separated chat ids to always use openai models (no need `/gpt`)
236 DEEPSEEK_CHATS = os.getenv("TID_DEEPSEEK_CHATS", "") # comma separated chat ids to always use deepseek models (no need `/ds`)
237 QWEN_CHATS = os.getenv("TID_QWEN_CHATS", "") # comma separated chat ids to always use qwen models (no need `/qwen`)
238 DOUBAO_CHATS = os.getenv("TID_DOUBAO_CHATS", "") # comma separated chat ids to always use doubao models (no need `/doubao`)
239 GROK_CHATS = os.getenv("TID_GROK_CHATS", "") # comma separated chat ids to always use grok models (no need `/grok`)
240 KIMI_CHATS = os.getenv("TID_KIMI_CHATS", "") # comma separated chat ids to always use kimi models (no need `/kimi`)
241
242
243class DB:
244 ENGINE = os.getenv("DB_ENGINE", "Cloudflare-R2")
245 CF_KV_ENABLED = os.getenv("CF_KV_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
246 CF_ACCOUNT_ID = os.getenv("CF_ACCOUNT_ID", "")
247 CF_API_TOKEN = os.getenv("CF_API_TOKEN", "")
248 CF_KV_NAMESPACE_ID = os.getenv("CF_KV_NAMESPACE_ID", "")
249 CF_R2_ENABLED = os.getenv("CF_R2_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
250 CF_R2_BUCKET_NAME = os.getenv("CF_R2_BUCKET_NAME", "bennybot")
251 CF_R2_ACCESS_KEY_ID = os.getenv("CF_R2_ACCESS_KEY_ID", "")
252 CF_R2_SECRET_ACCESS_KEY = os.getenv("CF_R2_SECRET_ACCESS_KEY", "")
253 CF_R2_PUBLIC_URL = os.getenv("CF_R2_PUBLIC_URL", "")
254 CF_D1_ENABLED = os.getenv("CF_D1_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
255 ALIST_ENABLED = os.getenv("ALIST_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
256 ALIST_USERNAME = os.getenv("ALIST_USERNAME", "guest")
257 ALIST_PASSWORD = os.getenv("ALIST_PASSWORD", "guest")
258 ALIST_SERVER = os.getenv("ALIST_SERVER", "")
259 ALIST_BASR_PATH = os.getenv("ALIST_BASR_PATH", "")
260 PASTBIN_SERVER = os.getenv("PASTBIN_SERVER", "https://shz.al")
261 PASTBIN_MAX_BYTES = int(os.getenv("PASTBIN_MAX_BYTES", "10485760")) # 10 MB
262 TURSO_ENABLED = os.getenv("TURSO_ENABLED", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
263 TURSO_USERNAME = os.getenv("TURSO_USERNAME", "") # https://turso.tech
264 TURSO_API_TOKEN = os.getenv("TURSO_API_TOKEN", "")
265 TURSO_GROUP_TOKEN = os.getenv("TURSO_GROUP_TOKEN", "")
266 GH_USER = os.getenv("DB_GH_USER", "")
267 GH_REPO = os.getenv("DB_GH_REPO", "bennybot") # just repo name, not `owner/repo`
268 GH_TOKEN = os.getenv("DB_GH_TOKEN", "")
269
270
271class HISTORY:
272 ENABLE = os.getenv("HISTORY_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
273 ENGINE = os.getenv("HISTORY_ENGINE", "turso").lower() # turso or d1 (This is for sync & backup)
274 QUERY_ENGINE = os.getenv("HISTORY_QUERY_ENGINE", "turso").lower() # turso or d1
275 TURSO_ENABLE = os.getenv("HISTORY_TURSO_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
276 TURSO_DATABASE = os.getenv("HISTORY_TURSO_DATABASE", "bennybot-history")
277 TURSO_USERNAME = os.getenv("HISTORY_TURSO_USERNAME", "") # https://turso.tech
278 TURSO_API_TOKEN = os.getenv("HISTORY_TURSO_API_TOKEN", "")
279 TURSO_GROUP_TOKEN = os.getenv("HISTORY_TURSO_GROUP_TOKEN", "")
280 PERIODICALLY_BACKUP_CHATS = os.getenv("HISTORY_PERIODICALLY_BACKUP_CHATS", "") # "full_table" or comma separated chat ids to include (without `-100` prefix)
281 BACKUP_CHATS_HOURS = float(os.getenv("HISTORY_BACKUP_CHATS_HOURS", "24")) # hours to backup chats
282 D1_ENABLE = os.getenv("HISTORY_D1_ENABLE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
283 D1_DATABASE = os.getenv("HISTORY_D1_DATABASE", "bennybot-history")
284 INCLUDE_CHATS = os.getenv("HISTORY_INCLUDE_CHATS", "") # "all" or comma separated chat ids to include (without `-100` prefix)
285 IGNORE_CHATS = os.getenv("HISTORY_IGNORE_CHATS", "") # comma separated chat ids to ignore (without `-100` prefix)
286 INCLUDE_PRIVATES = os.getenv("HISTORY_INCLUDE_PRIVATES", "") # "all" or comma separated private chat ids to include (without `-100` prefix)
287 INCLUDE_BOTS = os.getenv("HISTORY_INCLUDE_BOTS", "") # "all" or comma separated private chat ids to include (without `-100` prefix)
288 INCLUDE_GROUPS = os.getenv("HISTORY_INCLUDE_GROUPS", "") # "all" or comma separated private chat ids to include (without `-100` prefix)
289 INCLUDE_CHANNELS = os.getenv("HISTORY_INCLUDE_CHANNELS", "") # "all" or comma separated private chat ids to include (without `-100` prefix)
290
291
292class ASR:
293 # use different engines based on duration
294 # support ali, tencent, gemini, deepgram, cloudflare, groq
295 DEFAULT_ENGINE = os.getenv("ASR_DEFAULT_ENGINE", "auto")
296 SHORT_ENGINE = os.getenv("ASR_SHORT_ENGINE", "tencent") # comma separated engine names
297 SHORT_DURATION = int(os.getenv("ASR_SHORT_DURATION", "60"))
298 MIDDLE_ENGINE = os.getenv("ASR_MIDDLE_ENGINE", "tencent,ali") # comma separated engine names
299 MIDDLE_DURATION = int(os.getenv("ASR_MIDDLE_DURATION", "600"))
300 LONG_ENGINE = os.getenv("ASR_LONG_ENGINE", "gemini") # comma separated engine names
301
302 TENCENT_APPID = os.getenv("ASR_TENCENT_APPID", "")
303 TENCENT_SECRET_ID = os.getenv("ASR_TENCENT_SECRET_ID", "")
304 TENCENT_SECRET_KEY = os.getenv("ASR_TENCENT_SECRET_KEY", "")
305 TENCENT_FS_ENGINE = os.getenv("ASR_TENCENT_FS_ENGINE", "local") # local, uguu or alist.
306 # WARN: some models do not allow oversea VPS. Can upload to an alist server in China.
307 ALI_MODEL = os.getenv("ASR_ALI_MODEL", "paraformer-realtime-v2,paraformer-realtime-v1") # comma separated keys for load balance. e.g. "model1,model2,model3"
308 ALI_API_KEY = os.getenv("ASR_ALI_API_KEY", "") # comma separated keys for load balance. e.g. "key1,key2,key3"
309 # If the bot is running on an oversea VPS, and Ali ASR model doesn't allow oversea fileserver.
310 # Change ASR_ALI_FS_ENGINE to alist (configurations in DB class)
311 ALI_FS_ENGINE = os.getenv("ASR_ALI_FS_ENGINE", "local") # local, uguu or alist.
312 DEEPGRAM_API = os.getenv("ASR_DEEPGRAM_API", "") # comma separated keys for load balance. e.g. "key1,key2,key3"
313 CLOUDFLARE_MODEL = os.getenv("ASR_CLOUDFLARE_MODEL", "@cf/openai/whisper-large-v3-turbo")
314 CLOUDFLARE_CHUNK_SECONDS = float(os.getenv("ASR_CLOUDFLARE_CHUNK_SECONDS", "180")) # split long audio file into chunks
315 CLOUDFLARE_OVERLAP_SECONDS = float(os.getenv("ASR_CLOUDFLARE_OVERLAP_SECONDS", "5")) # overlap seconds between chunks
316 CLOUDFLARE_KEYS = os.getenv("ASR_CLOUDFLARE_KEYS", "") # comma separated keys for load balance. e.g. "AccountID:API_TOKEN, AccountID:API_TOKEN, ..."
317
318 GROQ_MAX_BYTES = int(os.getenv("ASR_GROQ_MAX_BYTES", "26214400")) # 25MB (max file bytes for single file)
319 GROQ_CHUNK_SECONDS = float(os.getenv("ASR_GROQ_CHUNK_SECONDS", "180")) # split long audio file into chunks
320 GROQ_OVERLAP_SECONDS = float(os.getenv("ASR_GROQ_OVERLAP_SECONDS", "5")) # overlap seconds between chunks
321 GROQ_KEYS = os.getenv("ASR_GROQ_KEYS", "") # comma separated keys for load balance.
322 GROQ_MODELS = os.getenv("ASR_GROQ_MODELS", "whisper-large-v3") # comma separated model names.
323
324 GEMINI_CHUNK_SECONDS = float(os.getenv("ASR_GEMINI_CHUNK_SECONDS", "600")) # split long audio file into chunks
325 GEMINI_OVERLAP_SECONDS = float(os.getenv("ASR_GEMINI_OVERLAP_SECONDS", "5")) # overlap seconds between chunks
326 GEMINI_MAX_DURATION = int(os.getenv("ASR_GEMINI_MAX_DURATION", "34200")) # 9.5 hour
327 GEMINI_MODEL = os.getenv("ASR_GEMINI_MODEL", "gemini-2.5-flash")
328 GEMINI_CONFIG = os.getenv("ASR_GEMINI_CONFIG", "{}") # default config passed to GenerateContentConfig. Should be a json string: '{"key": "value"}'
329
330
331class PODCAST:
332 FEED_URLS = os.getenv("PODCAST_FEED_URLS", "") # comma separated feed urls
333 OPML_URLS = os.getenv("PODCAST_OPML_URLS", "") # comma separated opml urls
334 YOUTUBE_CHANNEL_IDS = os.getenv("PODCAST_YOUTUBE_CHANNEL_IDS", "") # comma separated youtube channel ids
335 TID = int(os.getenv("PODCAST_TID", "0")) # send to this chat id
336 FS_ENGINE = os.getenv("PODCAST_FS_ENGINE", "CF-R2") # file storage engine for hosting podcast feeds
337 ASR_ENGINE = os.getenv("PODCAST_ASR_ENGINE", "auto") # default ASR engine
338 IGNORE_OLD_THAN_SECONDS = int(os.getenv("PODCAST_IGNORE_OLD_THAN_SECONDS", "14400")) # in seconds
339 KEEP_LATEST_ENTRIES = int(os.getenv("PODCAST_KEEP_LATEST_ENTRIES", "99999999")) # keep latest entries
340 # To bypass censorship, set asr engines here (comma separated titles or domains)
341 ASR_FORCE_GEMINI_TITLES = os.getenv("PODCAST_ASR_FORCE_GEMINI_TITLES", "")
342 ASR_FORCE_GEMINI_DOMAINS = os.getenv("PODCAST_ASR_FORCE_GEMINI_DOMAINS", "")
343 ASR_FORCE_GROQ_TITLES = os.getenv("PODCAST_ASR_FORCE_GROQ_TITLES", "")
344 ASR_FORCE_GROQ_DOMAINS = os.getenv("PODCAST_ASR_FORCE_GROQ_DOMAINS", "")
345 ASR_FORCE_CLOUDFLARE_TITLES = os.getenv("PODCAST_ASR_FORCE_CLOUDFLARE_TITLES", "")
346 ASR_FORCE_CLOUDFLARE_DOMAINS = os.getenv("PODCAST_ASR_FORCE_CLOUDFLARE_DOMAINS", "")
347 ASR_FORCE_WHISPER_TITLES = os.getenv("PODCAST_ASR_FORCE_WHISPER_TITLES", "")
348 ASR_FORCE_WHISPER_DOMAINS = os.getenv("PODCAST_ASR_FORCE_WHISPER_DOMAINS", "")
349 ASR_FORCE_UNCENSORED_TITLES = os.getenv("PODCAST_ASR_FORCE_UNCENSORED_TITLES", "")
350 ASR_FORCE_UNCENSORED_DOMAINS = os.getenv("PODCAST_ASR_FORCE_UNCENSORED_DOMAINS", "anchor.fm,feeds.acast.com")
351 GH_REPO = os.getenv("PODCAST_GH_REPO", "podcast")
352 GH_TOKEN = os.getenv("PODCAST_GH_TOKEN", "")
353
354
355class FAVORITE:
356 ENABLE_SEND = os.getenv("ENABLE_FAVORITE_SEND", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
357 ENABLE_SAVE = os.getenv("ENABLE_FAVORITE_SAVE", "1").lower() in ["1", "y", "yes", "t", "true", "on"]
358 SEND_PREFIX = os.getenv("FAVORITE_SEND_PREFIX", "/fav").lower()
359 SAVE_PREFIX = os.getenv("FAVORITE_SAVE_PREFIX", "/save").lower()
360 R2_PREFIX = os.getenv("FAVORITE_R2_PREFIX", "Favorite/")
361 BACKUP_CHAT = os.getenv("FAVORITE_BACKUP_CHAT", "") # chat id to backup favorite messages
362 TIDS_ALLOW_SEND = os.getenv("FAVORITE_TIDS_ALLOW_SEND", "all").lower() # or comma separated telegram uids
363 TIDS_ALLOW_SAVE = os.getenv("FAVORITE_TIDS_ALLOW_SAVE", "") # comma separated telegram uids
364
365
366class TTS:
367 # TTS related
368 DEFAULT_ENGINE = os.getenv("TTS_DEFAULT_ENGINE", "edge") # edge, gemini, qwen, sambert
369 GEMINI_MODEL = os.getenv("TTS_GEMINI_MODEL", "gemini-2.5-flash-preview-tts")
370 GEMINI_INPUT_TOKEN_LIMIT = int(os.getenv("TTS_GEMINI_INPUT_TOKEN_LIMIT", "8192")) # token limit of the tts model
371 GEMINI_SPLIT_LENGTH = int(os.getenv("TTS_GEMINI_SPLIT_LENGTH", "8192")) # split token limit of the tts model
372 GEMINI_VOICE = os.getenv("TTS_GEMINI_VOICE", "Sulafat")
373 ALI_API_KEY = os.getenv("TTS_ALI_API_KEY", "") # comma separated keys for load balance. e.g. "key1,key2,key3"
374 QWEN_MODEL = os.getenv("TTS_QWEN_MODEL", "qwen-tts,qwen-tts-latest") # comma separated keys for load balance.
375 QWEN_INPUT_TOKEN_LIMIT = int(os.getenv("TTS_QWEN_INPUT_TOKEN_LIMIT", "512")) # token limit of the tts model
376 QWEN_SPLIT_LENGTH = int(os.getenv("TTS_QWEN_SPLIT_LENGTH", "512")) # split token limit of the tts model
377 QWEN_VOICE = os.getenv("TTS_QWEN_VOICE", "Chelsie")
378 SAMBERT_MODEL = os.getenv("TTS_SAMBERT_MODEL", "ramdom") # comma separated models for load balance. use "random" to randomly choose a model
379 SAMBERT_LENGTH_LIMIT = int(os.getenv("TTS_SAMBERT_LENGTH_LIMIT", "20000")) # token limit of the tts model
380 EDGE_DOMAIN = os.getenv("TTS_EDGE_DOMAIN", "https://tts.wangwangit.com")
381 EDGE_VOICE = os.getenv("TTS_EDGE_VOICE", "晓晓")
382 EDGE_MODEL = os.getenv("TTS_EDGE_MODEL", "zh-CN-XiaoxiaoNeural")
383
384
385class AI:
386 # Text Generation
387 MAX_CONTEXTS_NUM = int(os.getenv("AI_MAX_CONTEXTS_NUM", "30"))
388 TEXT_MODEL_CONFIG_KEY = os.getenv("AI_MODEL_CONFIG_KEY", "AI-TEXT") # model configuration key in CF-KV
389 TEXT_GENERATION_DEFAULT_MODEL = os.getenv("AI_TEXT_GENERATION_DEFAULT_MODEL", "gemini")
390 GEMINI_MODEL_ID = os.getenv("AI_GEMINI_MODEL_ID", "gemini-2.5-flash")
391 GEMINI_API_KEYS = os.getenv("AI_GEMINI_API_KEYS", "") # comma separated keys for load balance. e.g. "key1,key2,key3"
392 GEMINI_BASE_URL = os.getenv("AI_GEMINI_BASE_URL", "https://generativelanguage.googleapis.com")
393 GEMINI_DEFAULT_HEADERS = os.getenv("AI_GEMINI_DEFAULT_HEADERS", "{}") # default headers passed to Gemini API. Should be a json string: '{"key": "value"}'
394 GEMINI_FILES_TTL = int(os.getenv("AI_GEMINI_FILES_TTL", "172800")) # clean gemini files after 48 hours
395
396 ANTHROPIC_MODEL_ID = os.getenv("AI_ANTHROPIC_MODEL_ID", "claude-opus-4-6")
397 ANTHROPIC_API_KEYS = os.getenv("AI_ANTHROPIC_API_KEYS", "") # comma separated keys for load balance. e.g. "key1,key2,key3"
398 ANTHROPIC_BASE_URL = os.getenv("AI_ANTHROPIC_BASE_URL", "https://api.anthropic.com")
399 ANTHROPIC_DEFAULT_HEADERS = os.getenv("AI_ANTHROPIC_DEFAULT_HEADERS", "{}") # default headers passed to Anthropic API. Should be a json string: '{"key": "value"}'
400 ANTHROPIC_FILES_TTL = int(os.getenv("AI_ANTHROPIC_FILES_TTL", "172800")) # clean anthropic files after 48 hours
401
402 OPENAI_MODEL_ID = os.getenv("AI_OPENAI_MODEL_ID", "gpt-4o")
403 OPENAI_API_KEYS = os.getenv("AI_OPENAI_API_KEYS", "") # comma separated keys for load balance. e.g. "key1,key2,key3"
404 OPENAI_BASE_URL = os.getenv("AI_OPENAI_BASE_URL", "https://api.openai.com/v1")
405 TOOL_CALL_MODEL_ALIAS = os.getenv("AI_TOOL_CALL_MODEL_ALIAS", "tool-call")
406 PODCAST_SUMMARY_MODEL_ALIAS = os.getenv("PODCAST_SUMMARY_MODEL_ALIAS", "podcast-summary")
407 SUBTITLE_SUMMARY_MODEL_ALIAS = os.getenv("SUBTITLE_SUMMARY_MODEL_ALIAS", "subtitle-summary")
408 CHAT_SUMMARY_MODEL_ALIAS = os.getenv("CHAT_SUMMARY_MODEL_ALIAS", "chat-summary")
409
410 # Image Generation
411 IMG_MODEL_CONFIG_KEY = os.getenv("AI_IMG_MODEL_CONFIG_KEY", "AI-IMG") # model configuration key in CF-KV
412 IMG_GENERATION_DEFAULT_MODEL = os.getenv("AI_IMG_GENERATION_DEFAULT_MODEL", "gpt")
413
414 # Video Generation
415 VIDEO_MODEL_CONFIG_KEY = os.getenv("AI_VIDEO_MODEL_CONFIG_KEY", "AI-VIDEO") # model configuration key in CF-KV
416 VIDEO_GENERATION_DEFAULT_MODEL = os.getenv("AI_VIDEO_GENERATION_DEFAULT_MODEL", "seedance")