linkgravity 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +160 -80
- package/bin/platforms.js +63 -0
- package/bin/setup.js +172 -76
- package/hooks/hook.py +3 -4
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/src/api/server.py +2 -4
- package/src/api/ui_routes.py +46 -16
- package/src/api/voice_routes.py +5 -8
- package/src/cogs/general_cog.py +4 -1
- package/src/cogs/voice_cog.py +4 -7
- package/src/config.py +9 -4
- package/src/core/agy_runner.py +3 -3
- package/src/core/session_manager.py +25 -0
- package/src/handlers/message_router.py +6 -9
- package/src/handlers/thread_reply.py +62 -34
- package/src/main.py +35 -305
- package/src/main_discord.py +311 -0
- package/src/main_telegram.py +250 -0
- package/src/messengers/base.py +46 -7
- package/src/messengers/discord_adapter.py +72 -5
- package/src/messengers/registry.py +28 -12
- package/src/messengers/telegram_adapter.py +372 -0
- package/src/services/audio_service.py +2 -2
- package/src/services/discord_helpers.py +3 -4
- package/src/services/response.py +2 -2
- package/src/services/streaming.py +12 -11
- package/src/services/discord_mcp.py +0 -50
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Telegram bot setup. Runs in the same process as Discord (see main.py),
|
|
2
|
+
which starts both concurrently when both platforms are enabled."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from telegram import BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, Update
|
|
11
|
+
from telegram.ext import Application, ApplicationBuilder, CallbackQueryHandler, CommandHandler, MessageHandler, filters
|
|
12
|
+
|
|
13
|
+
from config import TELEGRAM_TOKEN, allowed, bot_settings, logger, save_bot_settings, session_manager
|
|
14
|
+
from core.atomic_io import atomic_write_json, safe_load_json
|
|
15
|
+
from handlers.message_router import handle_message
|
|
16
|
+
from messengers.registry import register_adapter
|
|
17
|
+
from messengers.telegram_adapter import TelegramAdapter, markdown_to_telegram_html, safe_query_edit
|
|
18
|
+
from utils.utils import get_default_cwd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _start_session(chat_id: int, user_id: int) -> dict:
|
|
22
|
+
session = {
|
|
23
|
+
"status": "pending",
|
|
24
|
+
"platform": "telegram",
|
|
25
|
+
"user_id": user_id,
|
|
26
|
+
"cwd": get_default_cwd(),
|
|
27
|
+
"model": bot_settings.get("default_model") or None,
|
|
28
|
+
"conversation_id": None,
|
|
29
|
+
"created_at": datetime.now().isoformat(),
|
|
30
|
+
}
|
|
31
|
+
session_manager.set_session(str(chat_id), session)
|
|
32
|
+
return session
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def cmd_new(update: Update, context) -> None:
|
|
36
|
+
user = update.effective_user
|
|
37
|
+
chat_id = update.effective_chat.id
|
|
38
|
+
adapter: TelegramAdapter = context.bot_data["adapter"]
|
|
39
|
+
|
|
40
|
+
if not allowed(user.id, "telegram"):
|
|
41
|
+
await update.message.reply_text("❌ Permission denied.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if session_manager.get_session(str(chat_id)):
|
|
45
|
+
prompt_id = uuid.uuid4().hex[:12]
|
|
46
|
+
confirm_key, cancel_key = f"{prompt_id}:confirm", f"{prompt_id}:cancel"
|
|
47
|
+
keyboard = InlineKeyboardMarkup(
|
|
48
|
+
[
|
|
49
|
+
[
|
|
50
|
+
InlineKeyboardButton("✅ Yes, start new", callback_data=confirm_key),
|
|
51
|
+
InlineKeyboardButton("Cancel", callback_data=cancel_key),
|
|
52
|
+
]
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
await update.message.reply_text(
|
|
56
|
+
"⚠️ A session is already active in this chat. Starting a new one will lose its context. Continue?",
|
|
57
|
+
reply_markup=keyboard,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
async def confirm(query):
|
|
61
|
+
await query.answer()
|
|
62
|
+
await query.edit_message_text("🆕 Starting a new session...", reply_markup=None)
|
|
63
|
+
_start_session(chat_id, user.id)
|
|
64
|
+
await adapter.send_message(chat_id, "✅ **Ready for new session!** Send a message to begin.")
|
|
65
|
+
|
|
66
|
+
async def cancel(query):
|
|
67
|
+
await query.answer()
|
|
68
|
+
await query.edit_message_text("Cancelled.", reply_markup=None)
|
|
69
|
+
|
|
70
|
+
adapter.register_callback(confirm_key, confirm)
|
|
71
|
+
adapter.register_callback(cancel_key, cancel)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
_start_session(chat_id, user.id)
|
|
75
|
+
await update.message.reply_text(
|
|
76
|
+
markdown_to_telegram_html("✅ **Ready for new session!** Send a message to begin."), parse_mode="HTML"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def cmd_model(update: Update, context) -> None:
|
|
81
|
+
user = update.effective_user
|
|
82
|
+
chat_id = update.effective_chat.id
|
|
83
|
+
adapter: TelegramAdapter = context.bot_data["adapter"]
|
|
84
|
+
|
|
85
|
+
if not allowed(user.id, "telegram"):
|
|
86
|
+
await update.message.reply_text("❌ Permission Denied")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
session = session_manager.get_session(str(chat_id))
|
|
90
|
+
if not session:
|
|
91
|
+
await update.message.reply_text("⚠️ No active session here. Start one with /new first.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
from cogs.general_cog import load_cached_models
|
|
95
|
+
|
|
96
|
+
cached_models = load_cached_models()
|
|
97
|
+
current_model = session.get("model") or bot_settings.get("default_model")
|
|
98
|
+
|
|
99
|
+
def _apply_model(final_model: str) -> str:
|
|
100
|
+
session_manager.update_session(str(chat_id), "model", final_model)
|
|
101
|
+
bot_settings["default_model"] = final_model
|
|
102
|
+
save_bot_settings(bot_settings)
|
|
103
|
+
return final_model
|
|
104
|
+
|
|
105
|
+
if not context.args:
|
|
106
|
+
prompt_id = uuid.uuid4().hex[:12]
|
|
107
|
+
keyboard = []
|
|
108
|
+
for m in cached_models:
|
|
109
|
+
key = f"{prompt_id}:{m}"
|
|
110
|
+
label = ("✅ " if m == current_model else "") + m
|
|
111
|
+
|
|
112
|
+
async def pick(query, m=m):
|
|
113
|
+
final_model = _apply_model(m)
|
|
114
|
+
await query.answer()
|
|
115
|
+
await safe_query_edit(
|
|
116
|
+
query,
|
|
117
|
+
text=markdown_to_telegram_html(f"🤖 Model changed: **{final_model}**"),
|
|
118
|
+
parse_mode="HTML",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
adapter.register_callback(key, pick)
|
|
122
|
+
keyboard.append([InlineKeyboardButton(label, callback_data=key)])
|
|
123
|
+
|
|
124
|
+
await update.message.reply_text(
|
|
125
|
+
"Pick a model (or send /model <name> to type one):", reply_markup=InlineKeyboardMarkup(keyboard)
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
requested = " ".join(context.args)
|
|
130
|
+
exact = next((m for m in cached_models if m.lower() == requested.lower()), None)
|
|
131
|
+
partial = next((m for m in cached_models if requested.lower() in m.lower()), None)
|
|
132
|
+
final_model = _apply_model(exact or partial or requested)
|
|
133
|
+
|
|
134
|
+
await adapter.send_message(
|
|
135
|
+
chat_id, f"🤖 Model changed: **{final_model}**\n💾 Also set as the default for new sessions."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def cmd_credit(update: Update, context) -> None:
|
|
140
|
+
user = update.effective_user
|
|
141
|
+
adapter: TelegramAdapter = context.bot_data["adapter"]
|
|
142
|
+
|
|
143
|
+
if not allowed(user.id, "telegram"):
|
|
144
|
+
await update.message.reply_text("❌ Denied")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
settings_path = Path(os.getenv("HOME", "/root")) / ".gemini/antigravity-cli/settings.json"
|
|
148
|
+
current = bool(safe_load_json(settings_path, {}, logger=logger).get("useG1Credits", False))
|
|
149
|
+
|
|
150
|
+
async def set_credit(use_credits: bool, query) -> None:
|
|
151
|
+
try:
|
|
152
|
+
data = safe_load_json(settings_path, {}, logger=logger)
|
|
153
|
+
data["useG1Credits"] = use_credits
|
|
154
|
+
atomic_write_json(settings_path, data)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
await query.answer()
|
|
157
|
+
await safe_query_edit(query, text=f"⚠️ Failed to update settings: {e}")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
status_text = "🟢 **ON** (Using AI Credits)" if use_credits else "🔴 **OFF** (Using default/free model)"
|
|
161
|
+
await query.answer()
|
|
162
|
+
await safe_query_edit(
|
|
163
|
+
query,
|
|
164
|
+
text=markdown_to_telegram_html(f"✅ AI Credit setting updated: {status_text}"),
|
|
165
|
+
parse_mode="HTML",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
prompt_id = uuid.uuid4().hex[:12]
|
|
169
|
+
on_key, off_key = f"{prompt_id}:on", f"{prompt_id}:off"
|
|
170
|
+
adapter.register_callback(on_key, lambda query: set_credit(True, query))
|
|
171
|
+
adapter.register_callback(off_key, lambda query: set_credit(False, query))
|
|
172
|
+
|
|
173
|
+
keyboard = InlineKeyboardMarkup(
|
|
174
|
+
[
|
|
175
|
+
[
|
|
176
|
+
InlineKeyboardButton(("✅ " if current else "") + "🟢 ON", callback_data=on_key),
|
|
177
|
+
InlineKeyboardButton(("✅ " if not current else "") + "🔴 OFF", callback_data=off_key),
|
|
178
|
+
]
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
await update.message.reply_text("AI Credits:", reply_markup=keyboard)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def on_message(update: Update, context) -> None:
|
|
185
|
+
await handle_message(None, update, context.bot_data["adapter"])
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def on_error(update: object, context) -> None:
|
|
189
|
+
logger.exception(f"Unhandled exception in Telegram update: {context.error}")
|
|
190
|
+
if isinstance(update, Update) and update.effective_chat:
|
|
191
|
+
try:
|
|
192
|
+
adapter = context.bot_data["adapter"]
|
|
193
|
+
await adapter.send_message(
|
|
194
|
+
update.effective_chat.id,
|
|
195
|
+
"⚠️ **An internal bot error has occurred.** Please contact the developer or check the server logs.",
|
|
196
|
+
)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def on_ready(app: Application) -> None:
|
|
202
|
+
await app.bot.set_my_commands(
|
|
203
|
+
[
|
|
204
|
+
BotCommand("new", "Start a new session (or /start)"),
|
|
205
|
+
BotCommand("model", "Change the AI model for this session"),
|
|
206
|
+
BotCommand("credit", "Turn AI Credits on/off"),
|
|
207
|
+
]
|
|
208
|
+
)
|
|
209
|
+
logger.info(f"✅ Bot is fully online and ready! Logged in as @{app.bot.username}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_application() -> Application:
|
|
213
|
+
app = ApplicationBuilder().token(TELEGRAM_TOKEN).post_init(on_ready).concurrent_updates(True).build()
|
|
214
|
+
adapter = TelegramAdapter(app.bot)
|
|
215
|
+
app.bot_data["adapter"] = adapter
|
|
216
|
+
register_adapter("telegram", adapter)
|
|
217
|
+
|
|
218
|
+
app.add_handler(CommandHandler(["new", "start"], cmd_new))
|
|
219
|
+
app.add_handler(CommandHandler("model", cmd_model))
|
|
220
|
+
app.add_handler(CommandHandler("credit", cmd_credit))
|
|
221
|
+
app.add_handler(CallbackQueryHandler(adapter.handle_callback_query))
|
|
222
|
+
app.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, on_message))
|
|
223
|
+
app.add_error_handler(on_error)
|
|
224
|
+
return app
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def run_telegram(stop_event: asyncio.Event) -> None:
|
|
228
|
+
"""Runs the Telegram bot manually (not via Application.run_polling(),
|
|
229
|
+
which blocks and manages its own event loop) so it can run alongside
|
|
230
|
+
Discord in the same process. See PTB docs on combining Application
|
|
231
|
+
with other asyncio frameworks."""
|
|
232
|
+
if not TELEGRAM_TOKEN:
|
|
233
|
+
logger.critical("Missing TELEGRAM_TOKEN - set telegram_token in lgy.json first.")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
app = build_application()
|
|
237
|
+
logger.info("✅ Telegram bot starting (polling mode)...")
|
|
238
|
+
await app.initialize()
|
|
239
|
+
if app.post_init: # not auto-called outside run_polling()/run_webhook()
|
|
240
|
+
await app.post_init(app)
|
|
241
|
+
await app.start()
|
|
242
|
+
await app.updater.start_polling(allowed_updates=Update.ALL_TYPES)
|
|
243
|
+
try:
|
|
244
|
+
await stop_event.wait()
|
|
245
|
+
finally:
|
|
246
|
+
await app.updater.stop()
|
|
247
|
+
await app.stop()
|
|
248
|
+
await app.shutdown()
|
|
249
|
+
if app.post_shutdown: # same gotcha as post_init: not auto-called here
|
|
250
|
+
await app.post_shutdown(app)
|
package/src/messengers/base.py
CHANGED
|
@@ -9,9 +9,35 @@ this interface; see VoiceCapable.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
12
13
|
from contextlib import asynccontextmanager
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
from typing import Any
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class IncomingAttachment:
|
|
20
|
+
"""A file on an inbound message; reader defers fetching bytes until needed."""
|
|
21
|
+
|
|
22
|
+
filename: str
|
|
23
|
+
content_type: str | None
|
|
24
|
+
reader: Callable[[], Awaitable[bytes]]
|
|
25
|
+
|
|
26
|
+
async def read(self) -> bytes:
|
|
27
|
+
return await self.reader()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class IncomingMessage:
|
|
32
|
+
"""Platform-agnostic inbound message - adapters build this, handlers never see raw platform types."""
|
|
33
|
+
|
|
34
|
+
author_id: Any
|
|
35
|
+
platform: str
|
|
36
|
+
content: str
|
|
37
|
+
conversation_id: str
|
|
38
|
+
conversation_ref: Any
|
|
39
|
+
attachments: list[IncomingAttachment] = field(default_factory=list)
|
|
40
|
+
add_reaction: Callable[[str], Awaitable[None]] | None = None
|
|
15
41
|
|
|
16
42
|
|
|
17
43
|
@dataclass
|
|
@@ -42,6 +68,12 @@ class PromptHandle(ABC):
|
|
|
42
68
|
|
|
43
69
|
class MessengerAdapter(ABC):
|
|
44
70
|
platform_name: str = "unknown"
|
|
71
|
+
supports_renaming: bool = True # False skips the AI title-generation call - its result would go nowhere anyway
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def to_incoming_message(self, raw_event: Any) -> IncomingMessage | None:
|
|
75
|
+
"""Converts a native platform event to IncomingMessage, or None to ignore it (bot/system messages)."""
|
|
76
|
+
raise NotImplementedError
|
|
45
77
|
|
|
46
78
|
@abstractmethod
|
|
47
79
|
async def send_message(self, conversation_ref: Any, text: str) -> Any:
|
|
@@ -98,8 +130,15 @@ class MessengerAdapter(ABC):
|
|
|
98
130
|
return isinstance(self, VoiceCapable)
|
|
99
131
|
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
async def join_voice(self, guild_ref: Any, channel_ref: Any) -> None:
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
class VoiceCapable(ABC):
|
|
134
|
+
@abstractmethod
|
|
135
|
+
async def join_voice(self, guild_ref: Any, channel_ref: Any) -> None:
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
async def leave_voice(self, guild_ref: Any) -> None:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
async def play_tts(self, guild_ref: Any, audio_bytes: bytes) -> None:
|
|
144
|
+
raise NotImplementedError
|
|
@@ -9,6 +9,8 @@ import discord
|
|
|
9
9
|
|
|
10
10
|
from config import logger
|
|
11
11
|
from messengers.base import (
|
|
12
|
+
IncomingAttachment,
|
|
13
|
+
IncomingMessage,
|
|
12
14
|
MessengerAdapter,
|
|
13
15
|
PromptHandle,
|
|
14
16
|
ScopeOption,
|
|
@@ -17,6 +19,19 @@ from messengers.base import (
|
|
|
17
19
|
)
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
class _ErrorLoggingView(discord.ui.View):
|
|
23
|
+
async def on_error(self, interaction: discord.Interaction, error: Exception, item) -> None:
|
|
24
|
+
logger.exception(f"Discord button interaction error: {error}")
|
|
25
|
+
error_msg = "⚠️ **An error occurred while processing this button.** Please try again later or check the logs."
|
|
26
|
+
try:
|
|
27
|
+
if interaction.response.is_done():
|
|
28
|
+
await interaction.followup.send(error_msg, ephemeral=True)
|
|
29
|
+
else:
|
|
30
|
+
await interaction.response.send_message(error_msg, ephemeral=True)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
class _DiscordPromptHandle(PromptHandle):
|
|
21
36
|
def __init__(self, embed: discord.Embed, view: discord.ui.View):
|
|
22
37
|
self.embed = embed
|
|
@@ -25,7 +40,13 @@ class _DiscordPromptHandle(PromptHandle):
|
|
|
25
40
|
self.outcome: ToolApprovalOutcome | None = None
|
|
26
41
|
|
|
27
42
|
async def send(self, conversation_ref: discord.abc.Messageable) -> discord.Message:
|
|
28
|
-
self.
|
|
43
|
+
if self.embed.description and len(self.embed.description) > 4000:
|
|
44
|
+
self.embed.description = self.embed.description[:3997] + "..."
|
|
45
|
+
try:
|
|
46
|
+
self.message = await conversation_ref.send(embed=self.embed, view=self.view)
|
|
47
|
+
except discord.HTTPException as e:
|
|
48
|
+
logger.error(f"Failed to send Discord prompt message: {e}")
|
|
49
|
+
raise
|
|
29
50
|
return self.message
|
|
30
51
|
|
|
31
52
|
async def finalize(self) -> None:
|
|
@@ -47,10 +68,41 @@ class DiscordAdapter(MessengerAdapter):
|
|
|
47
68
|
def __init__(self, bot: discord.Client):
|
|
48
69
|
self.bot = bot
|
|
49
70
|
|
|
71
|
+
# -- inbound -------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def to_incoming_message(self, raw_event: discord.Message) -> IncomingMessage | None:
|
|
74
|
+
message = raw_event
|
|
75
|
+
if message.type not in (discord.MessageType.default, discord.MessageType.reply):
|
|
76
|
+
return None
|
|
77
|
+
if message.author.bot:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
attachments = [
|
|
81
|
+
IncomingAttachment(filename=att.filename, content_type=att.content_type, reader=att.read)
|
|
82
|
+
for att in message.attachments
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
async def add_reaction(emoji: str) -> None:
|
|
86
|
+
await message.add_reaction(emoji)
|
|
87
|
+
|
|
88
|
+
return IncomingMessage(
|
|
89
|
+
author_id=message.author.id,
|
|
90
|
+
platform=self.platform_name,
|
|
91
|
+
content=message.content,
|
|
92
|
+
conversation_id=str(message.channel.id),
|
|
93
|
+
conversation_ref=message.channel,
|
|
94
|
+
attachments=attachments,
|
|
95
|
+
add_reaction=add_reaction,
|
|
96
|
+
)
|
|
97
|
+
|
|
50
98
|
# -- plain messaging ---------------------------------------------------
|
|
51
99
|
|
|
52
100
|
async def send_message(self, conversation_ref: discord.abc.Messageable, text: str) -> discord.Message:
|
|
53
|
-
|
|
101
|
+
try:
|
|
102
|
+
return await conversation_ref.send(text)
|
|
103
|
+
except discord.HTTPException as e:
|
|
104
|
+
logger.error(f"Failed to send Discord message: {e}")
|
|
105
|
+
raise
|
|
54
106
|
|
|
55
107
|
async def edit_message(self, message_ref: discord.Message, text: str) -> bool:
|
|
56
108
|
try:
|
|
@@ -69,7 +121,11 @@ class DiscordAdapter(MessengerAdapter):
|
|
|
69
121
|
async def send_files(self, conversation_ref: discord.abc.Messageable, file_paths: list[str]) -> None:
|
|
70
122
|
if not file_paths:
|
|
71
123
|
return
|
|
72
|
-
|
|
124
|
+
try:
|
|
125
|
+
await conversation_ref.send(files=[discord.File(p) for p in file_paths])
|
|
126
|
+
except discord.HTTPException as e:
|
|
127
|
+
logger.error(f"Failed to send Discord files: {e}")
|
|
128
|
+
raise
|
|
73
129
|
|
|
74
130
|
def resolve_conversation(self, conversation_id: str) -> Any:
|
|
75
131
|
try:
|
|
@@ -98,7 +154,7 @@ class DiscordAdapter(MessengerAdapter):
|
|
|
98
154
|
scope_options: list[ScopeOption],
|
|
99
155
|
) -> PromptHandle:
|
|
100
156
|
embed = discord.Embed(title=title, description=body, color=discord.Color.orange())
|
|
101
|
-
view =
|
|
157
|
+
view = _ErrorLoggingView(timeout=None)
|
|
102
158
|
handle = _DiscordPromptHandle(embed, view)
|
|
103
159
|
|
|
104
160
|
def make_callback(decision: str, scope: ScopeOption | None):
|
|
@@ -154,7 +210,7 @@ class DiscordAdapter(MessengerAdapter):
|
|
|
154
210
|
description=f"**{question}**\n\nPlease select an answer below.",
|
|
155
211
|
color=discord.Color.blue(),
|
|
156
212
|
)
|
|
157
|
-
view =
|
|
213
|
+
view = _ErrorLoggingView(timeout=None)
|
|
158
214
|
handle = _DiscordPromptHandle(embed, view)
|
|
159
215
|
|
|
160
216
|
async def _resolve(interaction: discord.Interaction, text: str, note: str):
|
|
@@ -214,6 +270,17 @@ class DiscordAdapter(MessengerAdapter):
|
|
|
214
270
|
async def on_submit(modal_self, interaction: discord.Interaction):
|
|
215
271
|
await _resolve(interaction, modal_self.answer.value, "Selected (Write in)")
|
|
216
272
|
|
|
273
|
+
async def on_error(modal_self, interaction: discord.Interaction, error: Exception) -> None:
|
|
274
|
+
logger.exception(f"Discord modal submission error: {error}")
|
|
275
|
+
error_msg = "⚠️ **An error occurred while processing your response.** Please try again later or check the logs."
|
|
276
|
+
try:
|
|
277
|
+
if interaction.response.is_done():
|
|
278
|
+
await interaction.followup.send(error_msg, ephemeral=True)
|
|
279
|
+
else:
|
|
280
|
+
await interaction.response.send_message(error_msg, ephemeral=True)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
217
284
|
async def write_in_callback(interaction: discord.Interaction):
|
|
218
285
|
await interaction.response.send_modal(WriteInModal())
|
|
219
286
|
|
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
"""
|
|
2
|
-
main.py.
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Per-platform messenger adapter registry, populated once at startup by
|
|
2
|
+
main.py. Both bots now share one process, so callers must say which
|
|
3
|
+
platform they mean - either directly (voice/Discord-only code) or by
|
|
4
|
+
looking up which platform a given thread_id/conversation belongs to."""
|
|
5
5
|
|
|
6
6
|
from messengers.base import MessengerAdapter
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
_adapters: dict[str, MessengerAdapter] = {}
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
_adapter = adapter
|
|
11
|
+
def register_adapter(platform: str, adapter: MessengerAdapter) -> None:
|
|
12
|
+
_adapters[platform] = adapter
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
def
|
|
17
|
-
if
|
|
18
|
-
raise RuntimeError("
|
|
19
|
-
return
|
|
15
|
+
def get_adapter_for_platform(platform: str) -> MessengerAdapter:
|
|
16
|
+
if platform not in _adapters:
|
|
17
|
+
raise RuntimeError(f"No adapter registered for platform {platform!r} - is it enabled in lgy.json?")
|
|
18
|
+
return _adapters[platform]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_adapter_for_thread(thread_id: str) -> MessengerAdapter:
|
|
22
|
+
from config import session_manager
|
|
23
|
+
|
|
24
|
+
session = session_manager.get_session(thread_id) or {}
|
|
25
|
+
platform = session.get("platform", "discord") # pre-multi-platform sessions have no tag - assume discord
|
|
26
|
+
return get_adapter_for_platform(platform)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_adapter_for_conv_id(conv_id: str) -> MessengerAdapter | None:
|
|
30
|
+
from config import session_manager
|
|
31
|
+
|
|
32
|
+
for _thread_id, sess in session_manager.get_all_sessions().items():
|
|
33
|
+
if sess.get("conversation_id") == conv_id:
|
|
34
|
+
return get_adapter_for_platform(sess.get("platform", "discord"))
|
|
35
|
+
return None
|