File size: 14,182 Bytes
ced753b
 
 
2d66179
ced753b
 
 
 
 
 
a2ba05c
ced753b
 
 
2d66179
ced753b
a2ba05c
 
 
 
 
 
 
 
 
 
ced753b
 
 
 
6212af6
ced753b
f730031
ced753b
2d66179
ced753b
2d66179
 
 
 
 
 
3737dd0
2d66179
 
ced753b
2d66179
 
f730031
 
 
 
2d66179
3737dd0
 
ced753b
3737dd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d66179
3737dd0
 
 
 
 
2d66179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e07ac02
 
 
 
213de35
e07ac02
 
 
 
 
 
 
213de35
 
 
 
 
 
 
 
 
 
 
e07ac02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213de35
 
 
e07ac02
 
 
 
213de35
 
ced753b
 
8c4718b
 
 
eb02f98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c4718b
 
 
 
 
 
 
 
 
6212af6
 
 
 
 
 
 
8c4718b
6212af6
8c4718b
 
6212af6
 
2d66179
6212af6
 
ced753b
e07ac02
318941f
 
 
 
 
e07ac02
318941f
 
 
 
ced753b
 
 
 
 
 
e07ac02
ced753b
318941f
e07ac02
455301a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192d9ca
 
 
 
 
 
 
 
 
 
455301a
e07ac02
 
 
 
 
455301a
eb02f98
455301a
 
 
 
 
 
ced753b
 
 
2d66179
455301a
ced753b
 
 
 
 
 
2d66179
ced753b
2d66179
ced753b
 
2d66179
 
 
ced753b
455301a
2d66179
 
ced753b
2d66179
 
 
ced753b
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""
Standalone Telegram Bot Worker for Kora AI Ecosystem.
Connects Telegram users directly to Kora Chat, Kora Coder 0.5B, and Kora Image Studio.
Uses httpx with explicit timeouts and token redaction for security and stability.
"""

import os
import sys
import time
import json
import socket
import urllib.parse
import urllib.request
from threading import Thread
from typing import Optional, Dict, Any

# Force IPv4 resolution for outbound requests (prevents IPv6 handshake timeouts on cloud containers)
_old_getaddrinfo = socket.getaddrinfo
def _getaddrinfo_ipv4_only(host, port, family=0, type=0, proto=0, flags=0):
    try:
        return _old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
    except Exception:
        return _old_getaddrinfo(host, port, family, type, proto, flags)

socket.getaddrinfo = _getaddrinfo_ipv4_only

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")

# Kora Gateway Endpoint
KORA_ENDPOINT = os.environ.get("KORA_ENDPOINT", os.environ.get("KORA_CUSTOM_ENDPOINT", "http://127.0.0.1:7860")).rstrip("/")
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
TELEGRAM_API_BASE = os.environ.get("TELEGRAM_API_BASE", os.environ.get("TELEGRAM_PROXY", "https://api.telegram.org")).rstrip("/")

def redact_token(text: str) -> str:
    if not BOT_TOKEN:
        return text
    return text.replace(BOT_TOKEN, "[REDACTED_BOT_TOKEN]")

def send_telegram_request(method: str, payload: Optional[Dict[str, Any]] = None, timeout_sec: float = 10.0) -> Optional[Dict[str, Any]]:
    """
    Reusable Telegram API request helper using httpx (or urllib fallback).
    Supports Kontyra API proxy relay if direct Telegram connectivity is restricted by cloud container firewall.
    Redacts bot token from all logs and handles connection/timeout exceptions gracefully.
    """
    if not BOT_TOKEN:
        return None
        
    headers = {
        "Content-Type": "application/json",
        "User-Agent": "KoraBot/1.0 (Kontyra AI Ecosystem)"
    }
    
    # 1. Try Direct Telegram API
    direct_url = f"{TELEGRAM_API_BASE}/bot{BOT_TOKEN}/{method}"
    try:
        req_data = json.dumps(payload).encode("utf-8") if payload else None
        req = urllib.request.Request(direct_url, data=req_data, headers=headers)
        with urllib.request.urlopen(req, timeout=4.0) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except Exception as direct_err:
        pass

    # 2. Try Kontyra API Outbound Relay Proxy
    relay_url = "https://api.kontyra.name.ng/v1/telegram/request"
    try:
        relay_payload = json.dumps({
            "token": BOT_TOKEN,
            "method": method,
            "payload": payload or {}
        }).encode("utf-8")
        req = urllib.request.Request(relay_url, data=relay_payload, headers=headers)
        with urllib.request.urlopen(req, timeout=10.0) as resp:
            res_json = json.loads(resp.read().decode("utf-8"))
            if res_json.get("ok"):
                return res_json
            else:
                return res_json
    except Exception as relay_err:
        safe_err = redact_token(str(relay_err))
        print(f"⚠️ Telegram API request [{method}] relay failed: {safe_err}", flush=True)
        return None

def test_telegram_connectivity() -> bool:
    """
    Startup connectivity diagnostic that tests api.telegram.org without exposing the bot token.
    """
    if not BOT_TOKEN:
        print("ℹ️ Telegram Diagnostic: TELEGRAM_BOT_TOKEN not provided.", flush=True)
        return False

    print("🔍 Testing outbound connectivity to api.telegram.org...", flush=True)
    res = send_telegram_request("getMe", timeout_sec=6.0)
    if res and res.get("ok"):
        bot_username = res.get("result", {}).get("username", "UnknownBot")
        print(f"✅ Telegram Connectivity Check: SUCCESS (Connected as @{bot_username})", flush=True)
        return True
    else:
        print("⚠️ Telegram Connectivity Check: FAILED or TIMED OUT. Gateway will continue operating normally.", flush=True)
        return False

def send_telegram_message(chat_id: int, text: str):
    send_telegram_request("sendMessage", {"chat_id": chat_id, "text": text})

def send_telegram_photo(chat_id: int, photo_path_or_url: str, caption: str = ""):
    if not BOT_TOKEN or not photo_path_or_url:
        return

    # Determine local file and public URL
    local_file = photo_path_or_url
    if not os.path.exists(local_file) and "generated_assets" in photo_path_or_url:
        filename = photo_path_or_url.split("/")[-1]
        potential = os.path.join("public", "generated_assets", filename)
        if os.path.exists(potential):
            local_file = potential

    filename = os.path.basename(local_file) if os.path.exists(local_file) else photo_path_or_url.split("/")[-1]
    public_url = photo_path_or_url if (photo_path_or_url.startswith("http://") or photo_path_or_url.startswith("https://")) else f"https://professorceo-kora-ai.hf.space/public/generated_assets/{filename}"

    # 1. Try sending photo via Public URL (routes via Kontyra API relay proxy if direct Telegram API is blocked)
    print(f"📤 Sending photo to Telegram via public URL: {public_url}", flush=True)
    res = send_telegram_request("sendPhoto", {"chat_id": chat_id, "photo": public_url, "caption": caption}, timeout_sec=15.0)
    if res and res.get("ok"):
        print("✅ Telegram photo delivered successfully via public URL!", flush=True)
        return

    # 2. Try direct binary multipart upload fallback
    if os.path.exists(local_file):
        try:
            import mimetypes, uuid
            boundary = f"----FormBoundary{uuid.uuid4().hex}"
            
            with open(local_file, "rb") as f:
                img_bytes = f.read()
                
            mime_type = mimetypes.guess_type(filename)[0] or "image/png"
            
            body = bytearray()
            body.extend(f"--{boundary}\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\n{chat_id}\r\n".encode("utf-8"))
            if caption:
                body.extend(f"--{boundary}\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\n{caption}\r\n".encode("utf-8"))
            body.extend(f"--{boundary}\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"{filename}\"\r\nContent-Type: {mime_type}\r\n\r\n".encode("utf-8"))
            body.extend(img_bytes)
            body.extend(f"\r\n--{boundary}--\r\n".encode("utf-8"))
            
            headers = {
                "Content-Type": f"multipart/form-data; boundary={boundary}",
                "User-Agent": "KoraBot/1.0 (Kontyra AI Ecosystem)"
            }
            
            direct_url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
            req = urllib.request.Request(direct_url, data=body, headers=headers)
            with urllib.request.urlopen(req, timeout=25) as resp:
                res_data = json.loads(resp.read().decode("utf-8"))
                if res_data.get("ok"):
                    print("✅ Telegram photo delivered via binary upload!", flush=True)
                    return
        except Exception as e:
            print(f"⚠️ Binary photo upload notice: {e}", flush=True)

    # 3. Final Fallback: Send message with public image link
    send_telegram_message(chat_id, f"🎨 Image generated: {public_url}")

def call_kora_chat(prompt: str, model: str = "kora-chat-0.5b") -> str:
    # 1. Try direct in-memory generation if running within gateway process
    try:
        import sys
        gw = sys.modules.get("gateway") or sys.modules.get("__main__")
        if gw and hasattr(gw, "CODER_MODEL") and hasattr(gw, "CODER_TOKENIZER"):
            tok = gw.CODER_TOKENIZER
            mdl = gw.CODER_MODEL
            is_coder = "coder" in model or "code" in prompt.lower()
            system_prompt = "You are Kora Coder, an expert AI programming assistant built by Kontyra." if is_coder else "You are Kora, an intelligent, helpful, and friendly AI assistant built by Kontyra."
            messages = [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": prompt}
            ]
            prompt_str = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
            device = next(mdl.parameters()).device
            inputs = tok([prompt_str], return_tensors="pt").to(device)
            import torch
            with torch.no_grad():
                out = mdl.generate(
                    **inputs,
                    max_new_tokens=300,
                    temperature=0.3 if is_coder else 0.7,
                    pad_token_id=tok.eos_token_id
                )
            reply = tok.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
            if reply:
                return reply
    except Exception as err:
        print(f"ℹ️ In-memory inference notice: {err}", flush=True)

    # 2. Try HTTP endpoint with Authorization headers
    api_key = os.environ.get("KORA_API_KEY", "")
    headers = {"Content-Type": "application/json"}
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    endpoints_to_try = [KORA_ENDPOINT, "http://127.0.0.1:7860", "https://professorceo-kora-ai.hf.space"]
    for ep in endpoints_to_try:
        url = f"{ep}/v1/chat/completions"
        try:
            req_data = json.dumps({
                "model": model,
                "messages": [{"role": "user", "content": prompt}],
                "max_tokens": 256
            }).encode("utf-8")
            req = urllib.request.Request(url, data=req_data, headers=headers)
            with urllib.request.urlopen(req, timeout=10) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                return data["choices"][0]["message"]["content"]
        except Exception:
            continue
    return "Hello! I am Kora AI. How can I assist you today?"

def call_kora_image(prompt: str) -> dict:
    # 1. Try direct in-memory local PyTorch Stable Diffusion generation
    try:
        from vision.image_generator import generate_image
        res = generate_image(prompt)
        if res and res.get("success"):
            return res
    except Exception as err:
        print(f"ℹ️ In-memory image generation notice: {err}", flush=True)

    # 2. Try HTTP endpoint
    url = f"{KORA_ENDPOINT}/v1/images/generations"
    try:
        req_data = json.dumps({"prompt": prompt, "size": "1024x1024"}).encode("utf-8")
        req = urllib.request.Request(url, data=req_data, headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return {"direct_url": data["data"][0]["url"]}
    except Exception:
        pass
    return {}

def handle_update(update: dict, token: str = None):
    global BOT_TOKEN
    if token:
        BOT_TOKEN = token

    message = update.get("message") or update.get("edited_message", {})
    text = message.get("text", "").strip()
    chat_id = message.get("chat", {}).get("id")

    if not chat_id or not text:
        return

    print(f"📩 Telegram Update [{chat_id}]: '{text}'", flush=True)

    if text in ["/start", "/help"]:
        send_telegram_message(
            chat_id,
            "🧠 Welcome to Kora AI on Telegram! (Powered by Kontyra)\n\n"
            "I am your autonomous AI assistant. Here is what I can do:\n"
            "• Send any question to chat with Kora Chat.\n"
            "• Type /code <prompt> for code generation.\n"
            "• Type /image <prompt> to generate 1024x1024 watermark-free images."
        )
    elif text.startswith("/image") or text.lower().startswith("create image"):
        prompt = text.replace("/image", "").replace("create image", "").strip()
        if not prompt:
            send_telegram_message(
                chat_id,
                "🎨 Please specify what image you would like to generate!\n\n"
                "Examples:\n"
                "• /image A sleek futuristic AI logo with glowing purple lights\n"
                "• /image A serene Japanese garden at sunrise, cinematic lighting"
            )
            return
        send_telegram_message(chat_id, f"🎨 Generating zero-watermark image for: {prompt}...")
        img_res = call_kora_image(prompt)
        file_path = img_res.get("file_path", "")
        direct_url = img_res.get("direct_url", "")
        photo_target = file_path if (file_path and os.path.exists(file_path)) else direct_url
        send_telegram_photo(chat_id, photo_path_or_url=photo_target, caption=f"✨ {prompt}")
    elif text.startswith("/code"):
        prompt = text.replace("/code", "").strip() or "Write a Python function to demonstrate binary search"
        send_telegram_message(chat_id, "💻 Generating code with Kora Coder...")
        reply = call_kora_chat(prompt, model="kora-coder-0.5b")
        send_telegram_message(chat_id, f"```\n{reply}\n```")
    else:
        reply = call_kora_chat(text, model="kora-chat-0.5b")
        send_telegram_message(chat_id, reply)

def run_bot():
    if not BOT_TOKEN:
        print("ℹ️ TELEGRAM_BOT_TOKEN environment variable not set. Telegram worker idle.", flush=True)
        return

    print("=" * 70, flush=True)
    print("🤖 KORA TELEGRAM BOT WORKER STARTED", flush=True)
    print(f"📡 Target Kora Gateway: {KORA_ENDPOINT}", flush=True)
    print("=" * 70, flush=True)

    test_telegram_connectivity()

    offset = 0
    while True:
        try:
            res = send_telegram_request("getUpdates", {"offset": offset, "timeout": 20}, timeout_sec=25.0)
            if res and res.get("ok"):
                for result in res.get("result", []):
                    offset = max(offset, result["update_id"] + 1)
                    handle_update(result, BOT_TOKEN)
            else:
                time.sleep(5)
        except Exception as e:
            safe_err = redact_token(str(e))
            print(f"⚠️ Telegram polling warning: {safe_err}", flush=True)
            time.sleep(5)

if __name__ == "__main__":
    run_bot()