ilang-ai commited on
Commit
cc6f785
·
0 Parent(s):

v1.0: TelegramGuard - AI-powered group guardian with I-Lang Prompt Spec

Browse files

- Anti-spam: AI + repeat message detection (3x in 5min = ban)
- Vision: image and video thumbnail understanding
- Chat: group @mention and private conversation
- Prompts: .ilang format (I-Lang Prompt Spec)
- Deploy: HF Space (free) or VPS
- GitHub Actions: auto-sync to HuggingFace

.github/workflows/sync-hf.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to HuggingFace Space
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ sync:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ fetch-depth: 0
14
+ lfs: true
15
+
16
+ - name: Push to HuggingFace Space
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: |
20
+ git remote add hf https://i-Lang:${HF_TOKEN}@huggingface.co/spaces/i-Lang/TelegramGuard || true
21
+ git push hf main --force
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ prompts/
2
+ data/
3
+ __pycache__/
4
+ *.pyc
5
+ *.db
6
+ *.db-journal
7
+ *.db-wal
8
+ *.log
9
+ venv/
10
+ .env
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ RUN mkdir -p data
11
+
12
+ CMD ["python", "bot.py"]
README.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # I-Lang Guard
2
+
3
+ AI-powered Telegram group guardian. Anti-spam, vision, chat — all driven by [I-Lang Prompt Spec](https://github.com/ilang-ai/ilang-spec).
4
+
5
+ ## What It Does
6
+
7
+ - **Anti-spam**: AI detects ads, scams, political content, porn, and repeat flooding. Deletes messages and bans spammers automatically.
8
+ - **Vision**: Understands images and video thumbnails. Catches image-based spam too.
9
+ - **Chat**: @ the bot in any group for AI conversation.
10
+ - **Zero config**: Add to group → grant admin permissions → done.
11
+
12
+ ## Deploy Your Own (3 Steps)
13
+
14
+ ### Option A: HuggingFace Space (Free, Zero Server)
15
+
16
+ 1. Fork this repo
17
+ 2. Create a [HuggingFace Space](https://huggingface.co/new-space) (Docker SDK)
18
+ 3. Add secrets in Space Settings:
19
+
20
+ | Secret | How to get it |
21
+ |--------|--------------|
22
+ | `BOT_TOKEN` | Talk to [@BotFather](https://t.me/BotFather) on Telegram |
23
+ | `GEMINI_API_KEY` | Free at [aistudio.google.com](https://aistudio.google.com) |
24
+
25
+ ### Option B: VPS
26
+
27
+ ```bash
28
+ git clone https://github.com/ilang-ai/TelegramGuard.git
29
+ cd TelegramGuard
30
+ pip install -r requirements.txt
31
+ export BOT_TOKEN="your-token"
32
+ export GEMINI_API_KEY="your-key"
33
+ python bot.py
34
+ ```
35
+
36
+ ## Customize Your Bot
37
+
38
+ All prompts live in `prompts_demo/` as `.ilang` files:
39
+
40
+ | File | Controls |
41
+ |------|----------|
42
+ | `persona.ilang` | Bot personality, tone, rules |
43
+ | `antispam.ilang` | What counts as spam |
44
+ | `vision.ilang` | How to read images |
45
+
46
+ To customize: copy `prompts_demo/` to `prompts/`, edit the `.ilang` files. The bot loads `prompts/` first, falls back to `prompts_demo/`.
47
+
48
+ Prompts use [I-Lang Prompt Spec](https://github.com/ilang-ai/ilang-spec) — a structured format for AI instructions using GENE framework, IMMUNE rules, and layered architecture. Learn more at [ilang.ai](https://ilang.ai).
49
+
50
+ ## Architecture
51
+
52
+ ```
53
+ bot.py Main handler
54
+ modules/
55
+ chat.py AI calls (loads .ilang prompts)
56
+ db.py Single shared SQLite connection
57
+ database.py Schema
58
+ admin.py Group admin helpers
59
+ prompts_demo/ Open-source demo prompts (.ilang)
60
+ prompts/ Your custom prompts (.gitignore'd)
61
+ ```
62
+
63
+ ## BotFather Setup
64
+
65
+ After creating your bot, send these to [@BotFather](https://t.me/BotFather):
66
+
67
+ ```
68
+ /setjoingroups → Enable
69
+ /setprivacy → Disable (so bot can see all group messages)
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
75
+
76
+ ---
77
+
78
+ Powered by [I-Lang Prompt Spec](https://ilang.ai) | [Eastsoft Inc.](https://eastsoft.com)
bot.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ import os
4
+ import time as _time
5
+ import hashlib as _hashlib
6
+ import asyncio
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
11
+ from telegram.ext import (
12
+ Application, CommandHandler, MessageHandler,
13
+ CallbackQueryHandler, ChatMemberHandler, filters, ContextTypes
14
+ )
15
+ import config
16
+ from modules.db import db_exec
17
+ from modules.database import init_db
18
+ from modules.chat import (
19
+ ai_text, ai_vision, ai_voice, ai_group_reply,
20
+ ai_judge_group_message, ai_judge_group_image, ai_group_vision,
21
+ GROUP_WELCOME
22
+ )
23
+ from modules.admin import is_admin, is_bot_admin, register_group
24
+
25
+ logging.basicConfig(
26
+ format="%(asctime)s [%(levelname)s] %(message)s",
27
+ level=logging.INFO
28
+ )
29
+ logger = logging.getLogger(__name__)
30
+
31
+ TOS_TEXT = (
32
+ "I-Lang Guard 服务条款\n\n"
33
+ "为保障群组安全, 本Bot提供以下服务:\n"
34
+ "- 自动识别并清理垃圾广告\n"
35
+ "- 通过消息分析持续优化AI反spam能力\n\n"
36
+ "使用说明:\n"
37
+ "- Bot会分析群内消息用于反垃圾和AI模型优化\n"
38
+ "- 不存储个人身份信息\n"
39
+ "- 管理员可随时移除Bot终止服务\n\n"
40
+ "群管理员点击下方按钮即表示同意以上条款"
41
+ )
42
+
43
+
44
+ async def check_tos(chat_id):
45
+ async def _do(db):
46
+ cur = await db.execute("SELECT 1 FROM tos_consent WHERE chat_id=?", (chat_id,))
47
+ return await cur.fetchone() is not None
48
+ return await db_exec(_do)
49
+
50
+ async def record_tos(chat_id, user_id):
51
+ async def _do(db):
52
+ await db.execute(
53
+ "INSERT OR REPLACE INTO tos_consent (chat_id, accepted_by) VALUES (?, ?)",
54
+ (chat_id, user_id)
55
+ )
56
+ await db.commit()
57
+ await db_exec(_do)
58
+
59
+ async def delete_tos(chat_id):
60
+ async def _do(db):
61
+ await db.execute("DELETE FROM tos_consent WHERE chat_id=?", (chat_id,))
62
+ await db.commit()
63
+ await db_exec(_do)
64
+
65
+
66
+ def _ctx_info(context):
67
+ parts = []
68
+ history = context.user_data.get("history", [])
69
+ if not history:
70
+ parts.append("NEW_SESSION:新对话开始,主动问好,问用户今天需要什么帮助")
71
+ return " | ".join(parts)
72
+
73
+
74
+ async def _handle_ai_result(intent, device, reply, msg, user_id, context):
75
+ history = context.user_data.setdefault("history", [])
76
+ await msg.reply_text(reply)
77
+ history.append({"role": "assistant", "text": reply})
78
+ if len(history) > 20:
79
+ history[:] = history[-20:]
80
+
81
+
82
+ # ==================== Commands ====================
83
+
84
+ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
85
+ if not config.ADMIN_USER_ID:
86
+ config.ADMIN_USER_ID = update.effective_user.id
87
+ if update.effective_chat.type == "private":
88
+ context.user_data["history"] = []
89
+ intent, device, reply = await ai_text(
90
+ "/start",
91
+ history=None,
92
+ context_info="NEW_SESSION:用户刚打开对话,简短问好,介绍你能做什么"
93
+ )
94
+ await update.message.reply_text(reply)
95
+ context.user_data.setdefault("history", []).append({"role": "assistant", "text": reply})
96
+ else:
97
+ chat_id = update.effective_chat.id
98
+ await register_group(chat_id, update.effective_chat.title)
99
+ if not await check_tos(chat_id):
100
+ keyboard = InlineKeyboardMarkup([
101
+ [InlineKeyboardButton("同意并启用", callback_data="tos_accept_" + str(chat_id))],
102
+ [InlineKeyboardButton("不同意", callback_data="tos_decline_" + str(chat_id))]
103
+ ])
104
+ await update.message.reply_text(TOS_TEXT, reply_markup=keyboard)
105
+ else:
106
+ await update.message.reply_text(GROUP_WELCOME)
107
+
108
+
109
+ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
110
+ if update.effective_chat.type == "private":
111
+ await update.message.reply_text(
112
+ "直接跟我说话就行, 不用命令\n\n"
113
+ "群管理 → 拉我进群, 给管理员权限\n"
114
+ "其他 → 随便聊"
115
+ )
116
+ else:
117
+ await update.message.reply_text(
118
+ "我在群里自动工作, 不用配置\n\n管理员命令:\n/ban — 回复消息踢人"
119
+ )
120
+
121
+
122
+ async def cmd_ban(update: Update, context: ContextTypes.DEFAULT_TYPE):
123
+ if update.effective_chat.type == "private" or not update.message.reply_to_message:
124
+ return
125
+ if not await is_admin(update, context):
126
+ return
127
+ try:
128
+ t = update.message.reply_to_message.from_user
129
+ await context.bot.ban_chat_member(update.effective_chat.id, t.id)
130
+ await update.message.reply_text("done")
131
+ except Exception as e:
132
+ await update.message.reply_text(str(e))
133
+
134
+
135
+ # ==================== Group ====================
136
+
137
+ async def handle_group_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
138
+ msg = update.message
139
+ if not msg:
140
+ return
141
+ chat_id = msg.chat.id
142
+ user = msg.from_user
143
+ if not user:
144
+ return
145
+ text = msg.text or msg.caption or ""
146
+
147
+ await register_group(chat_id, msg.chat.title)
148
+
149
+ tos_ok = await check_tos(chat_id)
150
+
151
+ # @mention or reply to bot
152
+ is_mention = text and context.bot.username and ("@" + context.bot.username) in text
153
+ is_reply_to_bot = msg.reply_to_message and msg.reply_to_message.from_user and msg.reply_to_message.from_user.id == context.bot.id
154
+ has_media = bool(msg.photo or msg.video or msg.document)
155
+ if (text or has_media) and (is_mention or is_reply_to_bot):
156
+ clean = text.replace("@" + context.bot.username, "").strip() if (text and is_mention) else (text.strip() if text else "")
157
+ if not tos_ok:
158
+ reply = "我还没被启用哦, 请管理员点一下上面的「同意并启用」按钮"
159
+ else:
160
+ g_history = context.chat_data.setdefault("group_history", [])
161
+ if msg.photo:
162
+ try:
163
+ f = await context.bot.get_file(msg.photo[-1].file_id)
164
+ img_data = bytes(await f.download_as_bytearray())
165
+ reply = await ai_group_vision(img_data, caption=clean, history=g_history)
166
+ except Exception:
167
+ reply = "图片没看清, 再发一张?"
168
+ elif msg.video:
169
+ if msg.video.thumbnail:
170
+ try:
171
+ vf = await context.bot.get_file(msg.video.thumbnail.file_id)
172
+ vimg = bytes(await vf.download_as_bytearray())
173
+ reply = await ai_group_vision(vimg, caption=clean, history=g_history)
174
+ except Exception:
175
+ if clean:
176
+ g_history.append({"role": "user", "text": "[视频] " + clean})
177
+ reply = await ai_group_reply("[视频] " + clean, g_history)
178
+ else:
179
+ reply = "视频封面没看清, 说说是什么内容?"
180
+ elif clean:
181
+ g_history.append({"role": "user", "text": "[视频] " + clean})
182
+ reply = await ai_group_reply("[视频] " + clean, g_history)
183
+ else:
184
+ reply = "视频我看不了, 说说是什么内容?"
185
+ else:
186
+ g_history.append({"role": "user", "text": clean})
187
+ reply = await ai_group_reply(clean, g_history)
188
+ g_history.append({"role": "assistant", "text": reply})
189
+ if len(g_history) > 20:
190
+ g_history[:] = g_history[-20:]
191
+ await msg.reply_text(reply)
192
+ return
193
+
194
+ if not tos_ok:
195
+ return
196
+
197
+ # Admin check
198
+ is_admin_user = False
199
+ try:
200
+ member = await context.bot.get_chat_member(chat_id, user.id)
201
+ if member.status in ("administrator", "creator"):
202
+ is_admin_user = True
203
+ except Exception:
204
+ pass
205
+
206
+ # Track recent messages: (msg_id, content_hash, timestamp)
207
+ user_msgs = context.chat_data.setdefault("user_recent_msgs", {})
208
+ uid = user.id
209
+ if uid not in user_msgs:
210
+ user_msgs[uid] = []
211
+ content_hash = _hashlib.md5(text.encode()).hexdigest() if text else ""
212
+ now = _time.time()
213
+ user_msgs[uid].append((msg.message_id, content_hash, now))
214
+ if len(user_msgs[uid]) > 20:
215
+ user_msgs[uid] = user_msgs[uid][-20:]
216
+
217
+ # Admin bypass
218
+ if is_admin_user:
219
+ return
220
+
221
+ # Duplicate message detection
222
+ spam = False
223
+ if content_hash:
224
+ recent_same = [
225
+ e for e in user_msgs[uid]
226
+ if e[1] == content_hash and (now - e[2]) < config.SPAM_REPEAT_WINDOW
227
+ ]
228
+ if len(recent_same) >= config.SPAM_REPEAT_THRESHOLD:
229
+ spam = True
230
+ logger.info("REPEAT SPAM: user=" + str(uid) + " chat=" + str(chat_id) + " count=" + str(len(recent_same)))
231
+
232
+ # AI spam check (skip if already caught)
233
+ if not spam:
234
+ if msg.photo:
235
+ try:
236
+ f = await context.bot.get_file(msg.photo[-1].file_id)
237
+ data = bytes(await f.download_as_bytearray())
238
+ spam = await ai_judge_group_image(data, text)
239
+ except Exception:
240
+ if text:
241
+ spam = await ai_judge_group_message(text)
242
+ elif msg.video:
243
+ if msg.video.thumbnail:
244
+ try:
245
+ vf = await context.bot.get_file(msg.video.thumbnail.file_id)
246
+ vdata = bytes(await vf.download_as_bytearray())
247
+ spam = await ai_judge_group_image(vdata, text)
248
+ except Exception:
249
+ if text:
250
+ spam = await ai_judge_group_message(text)
251
+ elif text:
252
+ spam = await ai_judge_group_message(text)
253
+ elif msg.forward_date:
254
+ spam = True
255
+ elif msg.document or msg.sticker:
256
+ if text:
257
+ spam = await ai_judge_group_message(text)
258
+ elif msg.forward_date:
259
+ spam = True
260
+ elif text:
261
+ spam = await ai_judge_group_message(text)
262
+
263
+ if spam:
264
+ try:
265
+ tasks = []
266
+ for entry in user_msgs.get(uid, []):
267
+ mid = entry[0] if isinstance(entry, tuple) else entry
268
+ tasks.append(context.bot.delete_message(chat_id, mid))
269
+ tasks.append(context.bot.ban_chat_member(chat_id, uid))
270
+ results = await asyncio.gather(*tasks, return_exceptions=True)
271
+ user_msgs.pop(uid, None)
272
+ logger.info("SPAM nuked: user=" + str(uid) + " chat=" + str(chat_id) + " tasks=" + str(len(tasks)))
273
+ except Exception as e:
274
+ logger.warning("Anti-spam action failed: " + str(e))
275
+ last_remind = context.bot_data.get("perm_remind_" + str(chat_id), 0)
276
+ if _time.time() - last_remind > 3600:
277
+ context.bot_data["perm_remind_" + str(chat_id)] = _time.time()
278
+ try:
279
+ await msg.reply_text(
280
+ "\u26a0\ufe0f 发现垃圾消息但我没权限处理\n\n"
281
+ "点群名字 → 管理员 → 添加管理员 → 找到我 → 打开「删除消息」和「封禁用户」→ 完成"
282
+ )
283
+ except Exception:
284
+ pass
285
+ return
286
+
287
+
288
+ # ==================== Private ====================
289
+
290
+ async def handle_private_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
291
+ msg = update.message
292
+ if not msg or not msg.text:
293
+ return
294
+ user_id = msg.from_user.id
295
+ history = context.user_data.setdefault("history", [])
296
+ history.append({"role": "user", "text": msg.text})
297
+ intent, device, reply = await ai_text(msg.text, history, _ctx_info(context))
298
+ await _handle_ai_result(intent, device, reply, msg, user_id, context)
299
+
300
+
301
+ async def handle_private_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
302
+ msg = update.message
303
+ if not msg or not msg.photo:
304
+ return
305
+ user_id = msg.from_user.id
306
+ history = context.user_data.setdefault("history", [])
307
+ caption = msg.caption or ""
308
+ history.append({"role": "user", "text": "[photo] " + caption if caption else "[photo]"})
309
+ try:
310
+ file = await context.bot.get_file(msg.photo[-1].file_id)
311
+ img_bytes = bytes(await file.download_as_bytearray())
312
+ except Exception:
313
+ await msg.reply_text("图片没收到, 再发一次?")
314
+ return
315
+ intent, device, reply = await ai_vision(img_bytes, caption, history, _ctx_info(context))
316
+ await _handle_ai_result(intent, device, reply, msg, user_id, context)
317
+
318
+
319
+ async def handle_private_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
320
+ msg = update.message
321
+ if not msg or not msg.voice:
322
+ return
323
+ user_id = msg.from_user.id
324
+ history = context.user_data.setdefault("history", [])
325
+ history.append({"role": "user", "text": "[voice]"})
326
+ try:
327
+ file = await context.bot.get_file(msg.voice.file_id)
328
+ audio_bytes = bytes(await file.download_as_bytearray())
329
+ mime = msg.voice.mime_type or "audio/ogg"
330
+ except Exception:
331
+ await msg.reply_text("语音没收到, 再说一次或者打字也行")
332
+ return
333
+ intent, device, reply = await ai_voice(audio_bytes, mime, history, _ctx_info(context))
334
+ await _handle_ai_result(intent, device, reply, msg, user_id, context)
335
+
336
+
337
+ # ==================== Events ====================
338
+
339
+ async def handle_my_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE):
340
+ result = update.my_chat_member
341
+ if not result:
342
+ return
343
+ chat_id = result.chat.id
344
+ old = result.old_chat_member.status if result.old_chat_member else "left"
345
+ new = result.new_chat_member.status if result.new_chat_member else "left"
346
+
347
+ if old in ("left", "kicked") and new in ("member", "administrator"):
348
+ await delete_tos(chat_id)
349
+ await register_group(chat_id, result.chat.title)
350
+ keyboard = InlineKeyboardMarkup([
351
+ [InlineKeyboardButton("同意并启用", callback_data="tos_accept_" + str(chat_id))],
352
+ [InlineKeyboardButton("不同意", callback_data="tos_decline_" + str(chat_id))]
353
+ ])
354
+ await context.bot.send_message(chat_id, TOS_TEXT, reply_markup=keyboard)
355
+ elif old in ("member", "administrator") and new in ("left", "kicked"):
356
+ await delete_tos(chat_id)
357
+
358
+
359
+ async def handle_tos_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
360
+ query = update.callback_query
361
+ if not query or not query.data:
362
+ return
363
+
364
+ is_accept = query.data.startswith("tos_accept_")
365
+ is_decline = query.data.startswith("tos_decline_")
366
+ if not is_accept and not is_decline:
367
+ return
368
+
369
+ chat_id = query.message.chat.id
370
+ user_id = query.from_user.id
371
+
372
+ try:
373
+ admins = await context.bot.get_chat_administrators(chat_id)
374
+ admin_ids = [a.user.id for a in admins]
375
+ if user_id not in admin_ids:
376
+ await query.answer("只有管理员可以操作", show_alert=True)
377
+ return
378
+ except Exception:
379
+ pass
380
+
381
+ if is_accept:
382
+ await record_tos(chat_id, user_id)
383
+ await query.answer("已启用")
384
+ has_perms = False
385
+ try:
386
+ bot_member = await context.bot.get_chat_member(chat_id, context.bot.id)
387
+ if hasattr(bot_member, 'can_delete_messages') and bot_member.can_delete_messages and hasattr(bot_member, 'can_restrict_members') and bot_member.can_restrict_members:
388
+ has_perms = True
389
+ except Exception:
390
+ pass
391
+
392
+ if has_perms:
393
+ await query.edit_message_text("I-Lang Guard 已启用 ✅\n\n垃圾广告我来清理, 全自动")
394
+ else:
395
+ await query.edit_message_text(
396
+ "I-Lang Guard 已启用\n\n"
397
+ "⚠️ 我还需要管理员权限才能干活:\n\n"
398
+ "1. 点群名字进入群资料\n"
399
+ "2. 点「管理员」\n"
400
+ "3. 点「添加管理员」\n"
401
+ "4. 找到 I-Lang Guard 点它\n"
402
+ "5. 打开「删除消息」和「封禁用户」\n"
403
+ "6. 点右上角「完成」"
404
+ )
405
+ else:
406
+ await query.answer("好的, 再见")
407
+ await query.edit_message_text("已退出群组")
408
+ await context.bot.leave_chat(chat_id)
409
+
410
+
411
+ # ==================== Main ====================
412
+
413
+ def main():
414
+ # Ensure data directory exists
415
+ os.makedirs(os.path.dirname(config.DB_PATH) or "data", exist_ok=True)
416
+
417
+ app = Application.builder().token(config.BOT_TOKEN).build()
418
+
419
+ for cmd, fn in [
420
+ ("start", cmd_start), ("help", cmd_help), ("ban", cmd_ban),
421
+ ]:
422
+ app.add_handler(CommandHandler(cmd, fn))
423
+
424
+ app.add_handler(ChatMemberHandler(handle_my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER))
425
+ app.add_handler(CallbackQueryHandler(handle_tos_callback, pattern="^tos_"))
426
+
427
+ # Group
428
+ app.add_handler(MessageHandler(
429
+ (filters.TEXT | filters.PHOTO | filters.VIDEO | filters.Document.ALL | filters.Sticker.ALL) & filters.ChatType.GROUPS & ~filters.COMMAND,
430
+ handle_group_message
431
+ ))
432
+
433
+ # Private
434
+ app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, handle_private_text))
435
+ app.add_handler(MessageHandler(filters.PHOTO & filters.ChatType.PRIVATE, handle_private_photo))
436
+ app.add_handler(MessageHandler(filters.VOICE & filters.ChatType.PRIVATE, handle_private_voice))
437
+
438
+ loop = asyncio.new_event_loop()
439
+ asyncio.set_event_loop(loop)
440
+ loop.run_until_complete(init_db())
441
+
442
+ logger.info("I-Lang Guard starting...")
443
+ app.run_polling(
444
+ drop_pending_updates=True,
445
+ allowed_updates=["message", "callback_query", "my_chat_member"]
446
+ )
447
+
448
+
449
+ if __name__ == "__main__":
450
+ main()
config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Telegram Bot
4
+ BOT_TOKEN = os.environ.get("BOT_TOKEN", "")
5
+
6
+ # Gemini AI
7
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
8
+ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
9
+
10
+ # Database
11
+ DB_PATH = os.environ.get("DB_PATH", "data/bot.db")
12
+
13
+ # Anti-spam
14
+ SPAM_NEWUSER_COOLDOWN = int(os.environ.get("SPAM_NEWUSER_COOLDOWN", "300"))
15
+ SPAM_REPEAT_THRESHOLD = int(os.environ.get("SPAM_REPEAT_THRESHOLD", "3"))
16
+ SPAM_REPEAT_WINDOW = int(os.environ.get("SPAM_REPEAT_WINDOW", "300"))
17
+
18
+ # Admin user ID (auto-detected from first /start)
19
+ ADMIN_USER_ID = None
modules/__init__.py ADDED
File without changes
modules/admin.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config
2
+ from modules.db import db_exec
3
+
4
+ async def is_admin(update, context):
5
+ try:
6
+ chat_id = update.effective_chat.id
7
+ user_id = update.effective_user.id
8
+ member = await context.bot.get_chat_member(chat_id, user_id)
9
+ return member.status in ("administrator", "creator")
10
+ except Exception:
11
+ return False
12
+
13
+ def is_bot_admin(user_id):
14
+ return config.ADMIN_USER_ID and user_id == config.ADMIN_USER_ID
15
+
16
+ async def register_group(chat_id, title):
17
+ async def _do(db):
18
+ await db.execute(
19
+ "INSERT INTO groups (chat_id, title) VALUES (?, ?) "
20
+ "ON CONFLICT(chat_id) DO UPDATE SET title=?",
21
+ (chat_id, title, title)
22
+ )
23
+ await db.commit()
24
+ await db_exec(_do)
modules/chat.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import random
4
+ import os
5
+ import google.generativeai as genai
6
+ import config
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ genai.configure(api_key=config.GEMINI_API_KEY)
11
+
12
+ # Load prompts from .ilang files (prompts/ if exists, else prompts_demo/)
13
+ def _load_prompt(name):
14
+ for d in ("prompts", "prompts_demo"):
15
+ path = os.path.join(os.path.dirname(os.path.dirname(__file__)), d, name)
16
+ if os.path.exists(path):
17
+ with open(path, "r", encoding="utf-8") as f:
18
+ return f.read()
19
+ return ""
20
+
21
+ SYSTEM_PROMPT = _load_prompt("persona.ilang")
22
+ ANTISPAM_TEXT_PROMPT = _load_prompt("antispam.ilang")
23
+ VISION_PROMPT = _load_prompt("vision.ilang")
24
+
25
+ GROUP_WELCOME = (
26
+ "I-Lang Guard 来了\n\n"
27
+ "垃圾广告我来清理, 全自动, 不用配置\n"
28
+ "有问题可以 @我\n\n"
29
+ "给我管理员权限(删消息+封人)就行, 其他不用管"
30
+ )
31
+
32
+ model = genai.GenerativeModel(config.GEMINI_MODEL, system_instruction=SYSTEM_PROMPT)
33
+ vision_model = genai.GenerativeModel(config.GEMINI_MODEL)
34
+
35
+
36
+ def _parse(raw):
37
+ if not raw:
38
+ return ("chat", None, "...")
39
+ t = raw.strip()
40
+ if t.startswith("```"):
41
+ nl = t.find("\n")
42
+ t = t[nl + 1:] if nl > 0 else t[3:]
43
+ if t.endswith("```"):
44
+ t = t[:-3].strip()
45
+ try:
46
+ d = json.loads(t)
47
+ return (d.get("intent", "chat"), d.get("device"), d.get("reply", t))
48
+ except json.JSONDecodeError:
49
+ pass
50
+ last_brace = t.rfind("}")
51
+ while last_brace >= 0:
52
+ start = t.rfind("{", 0, last_brace)
53
+ if start >= 0:
54
+ try:
55
+ d = json.loads(t[start:last_brace + 1])
56
+ return (d.get("intent", "chat"), d.get("device"), d.get("reply", t))
57
+ except json.JSONDecodeError:
58
+ pass
59
+ last_brace = t.rfind("}", 0, last_brace)
60
+ for line in t.split("\n"):
61
+ line = line.strip()
62
+ if line and not line.startswith("{") and not line.startswith("taint") and not line.startswith("The "):
63
+ return ("chat", None, line)
64
+ return ("chat", None, "...")
65
+
66
+
67
+ def _ctx(history, info):
68
+ parts = []
69
+ if info:
70
+ parts.append("[ctx] " + info)
71
+ if history:
72
+ for h in history[-8:]:
73
+ r = "user" if h["role"] == "user" else "bot"
74
+ parts.append(r + ": " + h["text"])
75
+ return "\n".join(parts)
76
+
77
+
78
+ def _deflect():
79
+ lines = [
80
+ "这个话题不太方便聊, 换一个吧",
81
+ "换个话题? 你今天有什么需要帮忙的?",
82
+ "这个超纲了, 聊点别的吧",
83
+ ]
84
+ return random.choice(lines)
85
+
86
+
87
+ async def ai_text(text, history=None, context_info=""):
88
+ try:
89
+ c = _ctx(history, context_info)
90
+ prompt = c + "\nuser: " + text if c else "user: " + text
91
+ r = await model.generate_content_async(prompt)
92
+ raw = r.text.strip() if r.text else ""
93
+ if not raw:
94
+ return ("chat", None, _deflect())
95
+ return _parse(raw)
96
+ except Exception as e:
97
+ logger.warning("AI text: " + str(e))
98
+ return ("chat", None, _deflect())
99
+
100
+
101
+ async def ai_vision(image_bytes, caption="", history=None, context_info=""):
102
+ try:
103
+ c = _ctx(history, context_info)
104
+ prompt = VISION_PROMPT + "\n" + c
105
+ if caption:
106
+ prompt += "\nuser: " + caption
107
+ r = await vision_model.generate_content_async([prompt, {"mime_type": "image/jpeg", "data": image_bytes}])
108
+ return _parse(r.text if r.text else "")
109
+ except Exception as e:
110
+ logger.warning("AI vision: " + str(e))
111
+ return ("chat", None, "图片没看清, 再发一张?")
112
+
113
+
114
+ async def ai_voice(audio_bytes, mime_type="audio/ogg", history=None, context_info=""):
115
+ try:
116
+ c = _ctx(history, context_info)
117
+ prompt = SYSTEM_PROMPT + "\n" + c + "\nUser sent a voice message:"
118
+ r = await vision_model.generate_content_async([prompt, {"mime_type": mime_type, "data": audio_bytes}])
119
+ return _parse(r.text if r.text else "")
120
+ except Exception as e:
121
+ logger.warning("AI voice: " + str(e))
122
+ return ("chat", None, "语音没听清, 再说一次或者打字都行")
123
+
124
+
125
+ async def ai_judge_group_message(text):
126
+ try:
127
+ prompt = ANTISPAM_TEXT_PROMPT + "\n\n消息内容: " + text[:1000]
128
+ r = await vision_model.generate_content_async(prompt)
129
+ result = r.text.strip().lower() if r.text else "ok"
130
+ return "spam" in result
131
+ except Exception:
132
+ return False
133
+
134
+
135
+ async def ai_judge_group_image(image_bytes, caption=""):
136
+ try:
137
+ prompt = ANTISPAM_TEXT_PROMPT + "\n\n判断这张图片是否是spam。只回复 spam 或 ok。"
138
+ if caption:
139
+ prompt += "\nCaption: " + caption[:500]
140
+ r = await vision_model.generate_content_async([prompt, {"mime_type": "image/jpeg", "data": image_bytes}])
141
+ result = r.text.strip().lower() if r.text else "ok"
142
+ return "spam" in result
143
+ except Exception:
144
+ return False
145
+
146
+
147
+ async def ai_group_vision(image_bytes, caption="", history=None):
148
+ try:
149
+ ctx = _ctx(history, "GROUP_CHAT: 用户在群里发了张图片@你, 简短评论1-2句话")
150
+ prompt = SYSTEM_PROMPT + "\n" + ctx
151
+ if caption:
152
+ prompt += "\nuser: " + caption
153
+ else:
154
+ prompt += "\nuser: [发了张图片]"
155
+ r = await vision_model.generate_content_async([prompt, {"mime_type": "image/jpeg", "data": image_bytes}])
156
+ raw = r.text.strip() if r.text else ""
157
+ if not raw:
158
+ return _deflect()
159
+ intent, device, reply = _parse(raw)
160
+ if reply in ("...", ""):
161
+ return _deflect()
162
+ return reply
163
+ except Exception:
164
+ return _deflect()
165
+
166
+
167
+ async def ai_group_reply(text, history=None):
168
+ try:
169
+ ctx = _ctx(history, "GROUP_CHAT: 你在群里被@了, 直接回答, 简短2句话")
170
+ prompt = ctx + "\nuser: " + text
171
+ r = await model.generate_content_async(prompt)
172
+ raw = r.text.strip() if r.text else ""
173
+ if not raw:
174
+ return _deflect()
175
+ intent, device, reply = _parse(raw)
176
+ if reply in ("...", ""):
177
+ return _deflect()
178
+ return reply
179
+ except Exception:
180
+ return _deflect()
modules/database.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.db import shared_db
2
+
3
+ async def init_db():
4
+ async with shared_db() as db:
5
+ await db.executescript("""
6
+ CREATE TABLE IF NOT EXISTS groups (
7
+ chat_id INTEGER PRIMARY KEY,
8
+ title TEXT,
9
+ antispam_on INTEGER DEFAULT 1,
10
+ added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
11
+ );
12
+ CREATE TABLE IF NOT EXISTS spam_log (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ chat_id INTEGER,
15
+ user_id INTEGER,
16
+ username TEXT,
17
+ message TEXT,
18
+ action TEXT,
19
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
20
+ );
21
+ CREATE TABLE IF NOT EXISTS user_strikes (
22
+ chat_id INTEGER,
23
+ user_id INTEGER,
24
+ strikes INTEGER DEFAULT 0,
25
+ PRIMARY KEY (chat_id, user_id)
26
+ );
27
+ CREATE TABLE IF NOT EXISTS new_members (
28
+ chat_id INTEGER,
29
+ user_id INTEGER,
30
+ joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
31
+ PRIMARY KEY (chat_id, user_id)
32
+ );
33
+ CREATE TABLE IF NOT EXISTS tos_consent (
34
+ chat_id INTEGER PRIMARY KEY,
35
+ accepted_by INTEGER,
36
+ accepted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
37
+ );
38
+ """)
39
+ await db.commit()
modules/db.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiosqlite
2
+ import asyncio
3
+ import config
4
+
5
+ _db = None
6
+ _db_lock = asyncio.Lock()
7
+
8
+ async def get_db():
9
+ global _db
10
+ if _db is None:
11
+ _db = await aiosqlite.connect(config.DB_PATH)
12
+ await _db.execute("PRAGMA journal_mode=WAL")
13
+ await _db.execute("PRAGMA busy_timeout=10000")
14
+ await _db.commit()
15
+ return _db
16
+
17
+ async def db_exec(func):
18
+ async with _db_lock:
19
+ return await func(await get_db())
20
+
21
+ class SharedDB:
22
+ async def __aenter__(self):
23
+ await _db_lock.acquire()
24
+ return await get_db()
25
+ async def __aexit__(self, *args):
26
+ _db_lock.release()
27
+
28
+ def shared_db():
29
+ return SharedDB()
prompts_demo/antispam.ilang ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ::ILANG:GENE:ANTISPAM::v1.0::
2
+ # ::SCOPE{PUBLIC|OPEN_SOURCE}::
3
+ # ::PURPOSE{TelegramGuard spam detection rules}::
4
+
5
+ # --------------------------------------------------------
6
+ # Learn I-Lang Prompt Spec: https://github.com/ilang-ai/ilang-spec
7
+ # --------------------------------------------------------
8
+
9
+ ::GENE_IMMUTABLE{001, T:JUDGE_BEHAVIOR, A:JUDGE_CONTENT_ONLY⇒MISS_SPAM, G:ALL, Θ:ALWAYS}
10
+ ::GENE_IMMUTABLE{002, T:THREE_STEP_ANALYSIS, A:SHALLOW_SCAN⇒FALSE_NEGATIVE, G:ALL, Θ:ALWAYS}
11
+ ::GENE_IMMUTABLE{003, T:SEE_THROUGH_TRICKS, A:LITERAL_READ⇒EVASION, G:ALL, Θ:ALWAYS}
12
+
13
+ ::IMMUNE{UNICODE_VARIANT, DETECT}
14
+ ::IMMUNE{EMOJI_STUFFING, DETECT}
15
+ ::IMMUNE{VERTICAL_SPLIT, DETECT}
16
+ ::IMMUNE{HOMOPHONE_REPLACE, DETECT}
17
+
18
+ # --------------------------------------------------------
19
+ # TEXT SPAM JUDGE
20
+ # --------------------------------------------------------
21
+
22
+ You are a Telegram group anti-spam expert. Use three-step analysis:
23
+
24
+ Step 1: Surface read — what does this message say?
25
+ Step 2: Deep read — what is the real intent? Who sends messages like this? Would a normal group member talk this way?
26
+ Step 3: Judge based on patterns:
27
+ - Below-market goods + invoice + contact info = black market
28
+ - Forwarded from channel + contact/link = ad spam
29
+ - Earning/income/part-time + contact = scam
30
+ - Crypto/airdrop + link = crypto scam
31
+ - Sexual content/hookup = sexual spam
32
+ - Gambling/lottery = gambling spam
33
+ - Normal chat/question/complaint/emoji/reply = OK
34
+
35
+ Watch for evasion tricks:
36
+ - Unicode variants, fullwidth chars, Cyrillic lookalikes
37
+ - Emoji-stuffed text
38
+ - Vertical/split text
39
+ - Homophones and pinyin substitution
40
+
41
+ Reply only: spam or ok
42
+
43
+ # --------------------------------------------------------
44
+ # IMAGE SPAM JUDGE
45
+ # --------------------------------------------------------
46
+
47
+ For images, judge:
48
+ - Ad poster/QR code/contact info/price list = spam
49
+ - Scam screenshot/fake earnings = spam
50
+ - Sexual or explicit content = spam
51
+ - Political propaganda = spam
52
+ - Normal chat screenshot/meme/daily photo = ok
53
+
54
+ Reply only: spam or ok
prompts_demo/persona.ilang ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ::ILANG:GENE:PERSONA::v1.0::
2
+ # ::SCOPE{PUBLIC|OPEN_SOURCE}::
3
+ # ::PURPOSE{TelegramGuard default personality}::
4
+ # ::CUSTOMIZE{fork and edit to create your own bot personality}::
5
+
6
+ # --------------------------------------------------------
7
+ # Learn I-Lang Prompt Spec: https://github.com/ilang-ai/ilang-spec
8
+ # --------------------------------------------------------
9
+
10
+ ::GENE_IMMUTABLE{001, T:HELPFUL, A:IGNORE_USER⇒BROKEN, G:ALL, Θ:ALWAYS}
11
+ ::GENE_IMMUTABLE{002, T:SAFE_REPLY, A:OUTPUT_HARMFUL⇒SHUTDOWN, G:ALL, Θ:ALWAYS}
12
+ ::GENE_IMMUTABLE{003, T:CHINESE_FIRST, A:WRONG_LANG⇒SWITCH, G:ALL, Θ:DETECT_LANG}
13
+ ::GENE_IMMUTABLE{004, T:CONCISE, A:VERBOSE⇒TRIM, G:ALL, Θ:ALWAYS}
14
+
15
+ ::GENE_MUTABLE{101, T:FRIENDLY, G:ALL, Θ:BASE}
16
+ ::GENE_MUTABLE{102, T:TONE_ADAPTIVE, G:adaptive, Θ:INTERLOCUTOR}
17
+
18
+ ::IMMUNE{POLITICS, DEFLECT}
19
+ ::IMMUNE{ILLEGAL_CONTENT, REFUSE}
20
+
21
+ # --------------------------------------------------------
22
+ # PERSONA DEFINITION
23
+ # --------------------------------------------------------
24
+
25
+ You are TelegramGuard, a friendly AI assistant in Telegram.
26
+ Reply in Chinese by default. Keep replies to 2-3 sentences.
27
+
28
+ Your capabilities:
29
+ 1. Group management: auto spam detection and cleanup
30
+ 2. AI chat: answer questions, casual conversation
31
+ 3. Vision: understand images sent to you
32
+
33
+ Rules:
34
+ - Never discuss politics: reply with "这个话题不聊, 换一个吧"
35
+ - Never recommend paid services
36
+ - Never fabricate links
37
+
38
+ Response format (JSON):
39
+ {"intent": "chat", "device": null, "reply": "your reply text"}
40
+
41
+ intent options: chat | need_help | feedback
prompts_demo/vision.ilang ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ::ILANG:GENE:VISION::v1.0::
2
+ # ::SCOPE{PUBLIC|OPEN_SOURCE}::
3
+ # ::PURPOSE{TelegramGuard image understanding}::
4
+
5
+ # --------------------------------------------------------
6
+ # Learn I-Lang Prompt Spec: https://github.com/ilang-ai/ilang-spec
7
+ # --------------------------------------------------------
8
+
9
+ ::GENE_IMMUTABLE{001, T:OBSERVE_ALL, A:SKIP_DETAIL⇒MISS_CONTEXT, G:ALL, Θ:ALWAYS}
10
+ ::GENE_IMMUTABLE{002, T:DESCRIBE_CONCISE, A:VERBOSE⇒TRIM, G:ALL, Θ:ALWAYS}
11
+
12
+ # --------------------------------------------------------
13
+ # GROUP VISION
14
+ # --------------------------------------------------------
15
+
16
+ You are looking at an image shared in a Telegram group.
17
+ Describe what you see briefly in Chinese, 1-2 sentences.
18
+ Be natural, like a friend commenting on a photo.
19
+
20
+ Response format (JSON):
21
+ {"intent": "chat", "device": null, "reply": "your description"}
22
+
23
+ # --------------------------------------------------------
24
+ # TECH VISION
25
+ # --------------------------------------------------------
26
+
27
+ You are looking at a user's screenshot or photo in private chat.
28
+ Based on this image:
29
+ 1. What device/OS/app is shown?
30
+ 2. What is the user trying to do?
31
+ 3. Give ONE next action in Chinese, 2-3 sentences.
32
+
33
+ Response format (JSON):
34
+ {"intent": "chat", "device": null, "reply": "your guidance"}
35
+ device options: "ios" | "android" | "windows" | "mac" | null
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ python-telegram-bot[job-queue]>=21.0
2
+ google-generativeai>=0.8.0
3
+ aiosqlite>=0.20.0