Oth1 commited on
Commit
4d64981
·
verified ·
1 Parent(s): 92aa7b6

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +7 -7
  2. bot.py +225 -0
  3. requirements.txt +3 -3
Dockerfile CHANGED
@@ -1,16 +1,16 @@
1
  FROM python:3.11-slim
2
 
3
- ENV PYTHONUNBUFFERED=1 \
4
- PYTHONDONTWRITEBYTECODE=1
5
-
6
  WORKDIR /app
7
 
8
  COPY requirements.txt .
9
  RUN pip install --no-cache-dir -r requirements.txt
10
 
11
- COPY main.py .
12
 
13
- # Writable dir for the Telethon session file (HF Spaces containers run as non-root by default)
14
- RUN mkdir -p /app/data && chmod -R 777 /app
 
 
 
15
 
16
- CMD ["python", "main.py"]
 
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 bot.py .
9
 
10
+ # ملاحظة: مو لازم فتح بورت لأن هذا بوت polling (MTProto)
11
+ # مو ويب سيرفر، لكن نضيف واحد بسيط بس عشان Hugging Face
12
+ # يتأكد إن الـ Space "شغال" (health check).
13
+ ENV PORT=7860
14
+ EXPOSE 7860
15
 
16
+ CMD ["python", "bot.py"]
bot.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ import base64
5
+ import logging
6
+ from datetime import date
7
+
8
+ import requests
9
+ from pyrogram import Client, filters
10
+ from pyrogram.types import Message
11
+
12
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
13
+ log = logging.getLogger("image-bot")
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # الإعدادات (كلها تُقرأ من متغيرات البيئة / HF Space secrets)
17
+ # ---------------------------------------------------------------------------
18
+ API_ID = int(os.environ["API_ID"]) # من my.telegram.org
19
+ API_HASH = os.environ["API_HASH"] # من my.telegram.org
20
+ BOT_TOKEN = os.environ["BOT_TOKEN"] # من BotFather
21
+ GEMINI_API_KEY = os.environ["GEMINI_API_KEY"]
22
+ # آيدي مجموعة وحدة أو أكثر مفصولة بفاصلة، مثال: "-1001111111111,-1002222222222"
23
+ GROUP_IDS = [int(x.strip()) for x in os.environ["GROUP_IDS"].split(",") if x.strip()]
24
+
25
+ DAILY_LIMIT = 2
26
+ GEMINI_MODEL = "gemini-2.5-flash-image"
27
+ GEMINI_URL = (
28
+ f"https://generativelanguage.googleapis.com/v1beta/models/"
29
+ f"{GEMINI_MODEL}:generateContent?key={GEMINI_API_KEY}"
30
+ )
31
+
32
+ COUNTS_FILE = "counts.json"
33
+
34
+ app = Client(
35
+ "image_bot",
36
+ api_id=API_ID,
37
+ api_hash=API_HASH,
38
+ bot_token=BOT_TOKEN,
39
+ )
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # تخزين بسيط للعداد اليومي لكل مستخدم (يُحفظ بملف JSON)
43
+ # ملاحظة: تخزين الـ Space على Hugging Face مؤقت، إذا انعاد تشغيل الـ Space
44
+ # تنصفر العدادات. لو تحتاج ثبات دائم استخدم قاعدة بيانات خارجية لاحقاً.
45
+ # ---------------------------------------------------------------------------
46
+ def _load_counts() -> dict:
47
+ if os.path.exists(COUNTS_FILE):
48
+ try:
49
+ with open(COUNTS_FILE, "r", encoding="utf-8") as f:
50
+ return json.load(f)
51
+ except Exception:
52
+ return {}
53
+ return {}
54
+
55
+
56
+ def _save_counts(data: dict) -> None:
57
+ with open(COUNTS_FILE, "w", encoding="utf-8") as f:
58
+ json.dump(data, f)
59
+
60
+
61
+ def check_and_increment(user_id: int) -> bool:
62
+ """يرجع True إذا يقدر يسوي صورة، ويزيد العداد. يرجع False إذا وصل الحد."""
63
+ counts = _load_counts()
64
+ key = str(user_id)
65
+ today = date.today().isoformat()
66
+ entry = counts.get(key)
67
+
68
+ if not entry or entry.get("date") != today:
69
+ entry = {"date": today, "count": 0}
70
+
71
+ if entry["count"] >= DAILY_LIMIT:
72
+ counts[key] = entry
73
+ _save_counts(counts)
74
+ return False
75
+
76
+ entry["count"] += 1
77
+ counts[key] = entry
78
+ _save_counts(counts)
79
+ return True
80
+
81
+
82
+ def remaining_today(user_id: int) -> int:
83
+ counts = _load_counts()
84
+ entry = counts.get(str(user_id))
85
+ today = date.today().isoformat()
86
+ if not entry or entry.get("date") != today:
87
+ return DAILY_LIMIT
88
+ return max(0, DAILY_LIMIT - entry["count"])
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # استدعاء Gemini API
93
+ # ---------------------------------------------------------------------------
94
+ def gemini_generate(prompt: str) -> bytes | None:
95
+ """توليد صورة جديدة من نص فقط."""
96
+ body = {
97
+ "contents": [{"parts": [{"text": prompt}]}],
98
+ "generationConfig": {"responseModalities": ["IMAGE"]},
99
+ }
100
+ return _call_gemini(body)
101
+
102
+
103
+ def gemini_edit(image_bytes: bytes, prompt: str, mime_type: str = "image/jpeg") -> bytes | None:
104
+ """تعديل صورة موجودة حسب وصف نصي."""
105
+ b64 = base64.b64encode(image_bytes).decode("utf-8")
106
+ body = {
107
+ "contents": [
108
+ {
109
+ "parts": [
110
+ {"inline_data": {"mime_type": mime_type, "data": b64}},
111
+ {"text": prompt},
112
+ ]
113
+ }
114
+ ],
115
+ "generationConfig": {"responseModalities": ["IMAGE"]},
116
+ }
117
+ return _call_gemini(body)
118
+
119
+
120
+ def _call_gemini(body: dict) -> bytes | None:
121
+ try:
122
+ resp = requests.post(GEMINI_URL, json=body, timeout=120)
123
+ resp.raise_for_status()
124
+ data = resp.json()
125
+ except Exception as e:
126
+ log.error(f"Gemini API error: {e}")
127
+ return None
128
+
129
+ try:
130
+ parts = data["candidates"][0]["content"]["parts"]
131
+ for part in parts:
132
+ inline = part.get("inlineData") or part.get("inline_data")
133
+ if inline and inline.get("data"):
134
+ return base64.b64decode(inline["data"])
135
+ except Exception as e:
136
+ log.error(f"Gemini response parse error: {e} | raw: {data}")
137
+ return None
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # فلتر: فقط داخل المجموعة المحددة
142
+ # ---------------------------------------------------------------------------
143
+ in_allowed_group = filters.chat(GROUP_IDS)
144
+
145
+
146
+ @app.on_message(filters.command("start") & in_allowed_group)
147
+ async def start_cmd(_, message: Message):
148
+ await message.reply_text(
149
+ "أهلاً 👋\n"
150
+ f"- /image <وصف> لتوليد صورة جديدة (بحد أقصى {DAILY_LIMIT} باليوم لكل شخص)\n"
151
+ "- /edit <تعديل> كرد على صورة لتعديلها\n"
152
+ "- أو رد على أي صورة بنص التعديل مباشرة بدون كتابة /edit"
153
+ )
154
+
155
+
156
+ @app.on_message(filters.command("image") & in_allowed_group)
157
+ async def image_cmd(_, message: Message):
158
+ prompt = message.text.split(None, 1)
159
+ if len(prompt) < 2 or not prompt[1].strip():
160
+ await message.reply_text("اكتب وصف الصورة بعد الأمر، مثال:\n/image منظر جبال عند الغروب")
161
+ return
162
+
163
+ user_id = message.from_user.id
164
+ if not check_and_increment(user_id):
165
+ await message.reply_text(f"وصلت للحد اليومي ({DAILY_LIMIT} صور). جرب باچر 🙏")
166
+ return
167
+
168
+ status = await message.reply_text("⏳ جاري توليد الصورة...")
169
+ img_bytes = gemini_generate(prompt[1].strip())
170
+
171
+ if img_bytes is None:
172
+ await status.edit_text("❌ صار خطأ أثناء توليد الصورة، حاول مرة ثانية.")
173
+ return
174
+
175
+ await message.reply_photo(photo=io.BytesIO(img_bytes))
176
+ await status.delete()
177
+
178
+
179
+ @app.on_message(filters.command("edit") & in_allowed_group)
180
+ async def edit_cmd(_, message: Message):
181
+ if not message.reply_to_message or not message.reply_to_message.photo:
182
+ await message.reply_text("استخدم /edit كرد (reply) على صورة مع كتابة التعديل المطلوب.")
183
+ return
184
+
185
+ parts = message.text.split(None, 1)
186
+ if len(parts) < 2 or not parts[1].strip():
187
+ await message.reply_text("اكتب التعديل المطلوب بعد الأمر، مثال:\n/edit خلي الخلفية زرقاء")
188
+ return
189
+
190
+ await _do_edit(message, message.reply_to_message, parts[1].strip())
191
+
192
+
193
+ @app.on_message(filters.text & filters.reply & in_allowed_group & ~filters.command(["image", "edit", "start"]))
194
+ async def implicit_edit(_, message: Message):
195
+ """رد نصي (بدون أمر) على صورة = طلب تعديل."""
196
+ replied = message.reply_to_message
197
+ if not replied or not replied.photo:
198
+ return
199
+ await _do_edit(message, replied, message.text.strip())
200
+
201
+
202
+ async def _do_edit(message: Message, photo_message: Message, prompt: str):
203
+ user_id = message.from_user.id
204
+ if not check_and_increment(user_id):
205
+ await message.reply_text(f"وصلت للحد اليومي ({DAILY_LIMIT} صور). جرب باچر 🙏")
206
+ return
207
+
208
+ status = await message.reply_text("⏳ جاري تعديل الصورة...")
209
+
210
+ file_path = await photo_message.download(in_memory=True)
211
+ image_bytes = file_path.getvalue()
212
+
213
+ result_bytes = gemini_edit(image_bytes, prompt)
214
+
215
+ if result_bytes is None:
216
+ await status.edit_text("❌ صار خطأ أثناء تعديل الصورة، حاول مرة ثانية.")
217
+ return
218
+
219
+ await message.reply_photo(photo=io.BytesIO(result_bytes))
220
+ await status.delete()
221
+
222
+
223
+ if __name__ == "__main__":
224
+ log.info("Bot starting...")
225
+ app.run()
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- telethon==1.36.0
2
- motor==3.5.1
3
- pymongo==4.8.0
 
1
+ pyrogram==2.0.106
2
+ tgcrypto==1.2.5
3
+ requests==2.32.3