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")