linkgravity 1.4.0 → 1.5.0
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/README.md +54 -19
- package/bin/cli.js +27 -3
- package/bin/platforms.js +18 -1
- package/bin/setup.js +43 -0
- package/package.json +4 -2
- package/requirements.txt +1 -0
- package/src/cogs/general_cog.py +0 -21
- package/src/cogs/voice_cog.py +48 -4
- package/src/config.py +15 -2
- package/src/core/logger.py +6 -0
- package/src/core/platform_health.py +28 -0
- package/src/handlers/thread_reply.py +1 -1
- package/src/main.py +33 -7
- package/src/main_discord.py +3 -0
- package/src/main_slack.py +252 -0
- package/src/main_telegram.py +74 -23
- package/src/messengers/base.py +5 -0
- package/src/messengers/discord_adapter.py +19 -1
- package/src/messengers/slack_adapter.py +492 -0
- package/src/messengers/telegram_adapter.py +4 -4
- package/src/services/audio_service.py +6 -1
- package/src/services/streaming.py +6 -5
- package/voice-service/index.js +43 -193
package/src/main_discord.py
CHANGED
|
@@ -190,6 +190,9 @@ async def _supervise_voice_process(voice_dir: str):
|
|
|
190
190
|
@bot.event
|
|
191
191
|
async def on_ready():
|
|
192
192
|
logger.info(f"✅ Bot is fully online and ready! Logged in as {bot.user}")
|
|
193
|
+
from core import platform_health
|
|
194
|
+
|
|
195
|
+
platform_health.set_status("discord", "running")
|
|
193
196
|
|
|
194
197
|
|
|
195
198
|
def _terminate_voice_process():
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Slack bot setup. Runs in the same process as Discord/Telegram (see
|
|
2
|
+
main.py), which starts all enabled platforms concurrently."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
|
10
|
+
from slack_bolt.app.async_app import AsyncApp
|
|
11
|
+
from slack_sdk.errors import SlackApiError
|
|
12
|
+
|
|
13
|
+
from config import SLACK_APP_TOKEN, SLACK_BOT_TOKEN, allowed, bot_settings, logger, session_manager
|
|
14
|
+
from core import platform_health
|
|
15
|
+
from handlers.message_router import handle_message
|
|
16
|
+
from messengers.registry import register_adapter
|
|
17
|
+
from messengers.slack_adapter import SlackAdapter, encode_conversation_id, latest_channel_session
|
|
18
|
+
from utils.utils import get_default_cwd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _start_session(conversation_id: str, user_id: str) -> dict:
|
|
22
|
+
session = {
|
|
23
|
+
"status": "pending",
|
|
24
|
+
"platform": "slack",
|
|
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(conversation_id, session)
|
|
32
|
+
return session
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def cmd_new(ack, body, respond, context) -> None:
|
|
36
|
+
await ack()
|
|
37
|
+
user_id = body["user_id"]
|
|
38
|
+
channel = body["channel_id"]
|
|
39
|
+
|
|
40
|
+
if not allowed(user_id, "slack"):
|
|
41
|
+
await respond("❌ Permission denied.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if body.get("channel_name") == "directmessage":
|
|
45
|
+
# DMs are 1:1 like Telegram - no threading, so the channel itself is the session key.
|
|
46
|
+
conversation_id = encode_conversation_id(channel, channel)
|
|
47
|
+
_start_session(conversation_id, user_id)
|
|
48
|
+
await respond("✅ *Ready for new session!* Send a message to begin.")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
client = context["client"]
|
|
52
|
+
# A slash command has no message ts to thread under, and (unlike Discord) Slack has no
|
|
53
|
+
# explicit "create thread" call - so post the announcement first and thread off its ts.
|
|
54
|
+
try:
|
|
55
|
+
resp = await client.chat_postMessage(
|
|
56
|
+
channel=channel, text="✅ *New session started!* Reply in this thread to begin."
|
|
57
|
+
)
|
|
58
|
+
except SlackApiError as e:
|
|
59
|
+
if e.response.get("error") == "not_in_channel":
|
|
60
|
+
await respond("❌ I'm not in this channel yet - run `/invite @<this bot>` here first, then try /new again.")
|
|
61
|
+
else:
|
|
62
|
+
await respond(f"❌ Failed to start a session: {e.response.get('error')}")
|
|
63
|
+
return
|
|
64
|
+
conversation_id = encode_conversation_id(channel, resp["ts"])
|
|
65
|
+
_start_session(conversation_id, user_id)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def cmd_model(ack, body, respond, context) -> None:
|
|
69
|
+
await ack()
|
|
70
|
+
adapter: SlackAdapter = context["adapter"]
|
|
71
|
+
user_id = body["user_id"]
|
|
72
|
+
channel = body["channel_id"]
|
|
73
|
+
|
|
74
|
+
if not allowed(user_id, "slack"):
|
|
75
|
+
await respond("❌ Permission Denied")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
found = latest_channel_session(channel)
|
|
79
|
+
if not found:
|
|
80
|
+
await respond("⚠️ No active session here. Start one with /new first.")
|
|
81
|
+
return
|
|
82
|
+
conversation_id, session = found
|
|
83
|
+
|
|
84
|
+
from cogs.general_cog import load_cached_models
|
|
85
|
+
|
|
86
|
+
cached_models = load_cached_models()
|
|
87
|
+
current_model = session.get("model") or bot_settings.get("default_model")
|
|
88
|
+
|
|
89
|
+
def _apply_model(final_model: str) -> str:
|
|
90
|
+
session_manager.update_session(conversation_id, "model", final_model)
|
|
91
|
+
bot_settings["default_model"] = final_model
|
|
92
|
+
from config import save_bot_settings
|
|
93
|
+
|
|
94
|
+
save_bot_settings(bot_settings)
|
|
95
|
+
return final_model
|
|
96
|
+
|
|
97
|
+
requested = (body.get("text") or "").strip()
|
|
98
|
+
if not requested:
|
|
99
|
+
prompt_id = uuid.uuid4().hex[:12]
|
|
100
|
+
elements = []
|
|
101
|
+
for m in cached_models:
|
|
102
|
+
key = f"{prompt_id}:{m}"
|
|
103
|
+
label = ("✅ " if m == current_model else "") + m
|
|
104
|
+
|
|
105
|
+
async def pick(action_body, client, m=m):
|
|
106
|
+
final_model = _apply_model(m)
|
|
107
|
+
await client.chat_postMessage(channel=channel, text=f"🤖 Model changed: *{final_model}*")
|
|
108
|
+
|
|
109
|
+
adapter.register_callback(key, pick)
|
|
110
|
+
elements.append({"type": "button", "text": {"type": "plain_text", "text": label[:75]}, "action_id": key})
|
|
111
|
+
|
|
112
|
+
await respond(
|
|
113
|
+
{
|
|
114
|
+
"text": "Pick a model (or send /model <name> to type one):",
|
|
115
|
+
"blocks": [
|
|
116
|
+
{
|
|
117
|
+
"type": "section",
|
|
118
|
+
"text": {"type": "mrkdwn", "text": "Pick a model (or send `/model <name>` to type one):"},
|
|
119
|
+
},
|
|
120
|
+
{"type": "actions", "elements": elements[:25]},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
exact = next((m for m in cached_models if m.lower() == requested.lower()), None)
|
|
127
|
+
partial = next((m for m in cached_models if requested.lower() in m.lower()), None)
|
|
128
|
+
final_model = _apply_model(exact or partial or requested)
|
|
129
|
+
await respond(f"🤖 Model changed: *{final_model}*\n💾 Also set as the default for new sessions.")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def cmd_credit(ack, body, respond, context) -> None:
|
|
133
|
+
await ack()
|
|
134
|
+
adapter: SlackAdapter = context["adapter"]
|
|
135
|
+
user_id = body["user_id"]
|
|
136
|
+
channel = body["channel_id"]
|
|
137
|
+
|
|
138
|
+
if not allowed(user_id, "slack"):
|
|
139
|
+
await respond("❌ Denied")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
import os
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
|
|
145
|
+
from core.atomic_io import atomic_write_json, safe_load_json
|
|
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, action_body, client) -> 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 client.chat_postMessage(channel=channel, text=f"⚠️ Failed to update settings: {e}")
|
|
157
|
+
return
|
|
158
|
+
status_text = "🟢 *ON* (Using AI Credits)" if use_credits else "🔴 *OFF* (Using default/free model)"
|
|
159
|
+
await client.chat_postMessage(channel=channel, text=f"✅ AI Credit setting updated: {status_text}")
|
|
160
|
+
|
|
161
|
+
prompt_id = uuid.uuid4().hex[:12]
|
|
162
|
+
on_key, off_key = f"{prompt_id}:on", f"{prompt_id}:off"
|
|
163
|
+
adapter.register_callback(on_key, lambda b, c: set_credit(True, b, c))
|
|
164
|
+
adapter.register_callback(off_key, lambda b, c: set_credit(False, b, c))
|
|
165
|
+
|
|
166
|
+
await respond(
|
|
167
|
+
{
|
|
168
|
+
"text": "AI Credits:",
|
|
169
|
+
"blocks": [
|
|
170
|
+
{
|
|
171
|
+
"type": "actions",
|
|
172
|
+
"elements": [
|
|
173
|
+
{
|
|
174
|
+
"type": "button",
|
|
175
|
+
"text": {"type": "plain_text", "text": ("✅ " if current else "") + "🟢 ON"},
|
|
176
|
+
"action_id": on_key,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"type": "button",
|
|
180
|
+
"text": {"type": "plain_text", "text": ("✅ " if not current else "") + "🔴 OFF"},
|
|
181
|
+
"action_id": off_key,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def on_message(event, context) -> None:
|
|
191
|
+
await handle_message(None, event, context["adapter"])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def on_action(ack, body, context) -> None:
|
|
195
|
+
await ack()
|
|
196
|
+
await context["adapter"].handle_block_action(body)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def on_view_submission(ack, body, context) -> None:
|
|
200
|
+
await ack()
|
|
201
|
+
await context["adapter"].handle_view_submission(body)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def build_app() -> tuple[AsyncApp, SlackAdapter]:
|
|
205
|
+
app = AsyncApp(token=SLACK_BOT_TOKEN)
|
|
206
|
+
adapter = SlackAdapter(app)
|
|
207
|
+
|
|
208
|
+
@app.middleware
|
|
209
|
+
async def inject_adapter(context, next):
|
|
210
|
+
context["adapter"] = adapter
|
|
211
|
+
await next()
|
|
212
|
+
|
|
213
|
+
app.command("/new")(cmd_new)
|
|
214
|
+
app.command("/model")(cmd_model)
|
|
215
|
+
app.command("/credit")(cmd_credit)
|
|
216
|
+
app.event("message")(on_message)
|
|
217
|
+
app.action(re.compile(".*"))(on_action)
|
|
218
|
+
app.view(re.compile(".*"))(on_view_submission)
|
|
219
|
+
|
|
220
|
+
register_adapter("slack", adapter)
|
|
221
|
+
return app, adapter
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def run_slack(stop_event: asyncio.Event) -> None:
|
|
225
|
+
"""Uses connect_async()/close_async() directly - start_async() sleeps forever internally and never returns."""
|
|
226
|
+
if not SLACK_BOT_TOKEN or not SLACK_APP_TOKEN:
|
|
227
|
+
logger.critical(
|
|
228
|
+
"Missing SLACK_BOT_TOKEN/SLACK_APP_TOKEN - set slack_bot_token/slack_app_token in lgy.json first."
|
|
229
|
+
)
|
|
230
|
+
return
|
|
231
|
+
if not SLACK_BOT_TOKEN.startswith("xoxb-"):
|
|
232
|
+
logger.critical(
|
|
233
|
+
"slack_bot_token doesn't start with xoxb- - looks like the User OAuth Token was used "
|
|
234
|
+
"instead of the Bot User OAuth Token (OAuth & Permissions page has both)."
|
|
235
|
+
)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
app, adapter = build_app()
|
|
239
|
+
try:
|
|
240
|
+
await adapter.resolve_bot_user_id()
|
|
241
|
+
except SlackApiError as e:
|
|
242
|
+
logger.critical(f"Slack auth_test failed - check slack_bot_token: {e}")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
handler = AsyncSocketModeHandler(app, SLACK_APP_TOKEN)
|
|
246
|
+
logger.info("✅ Slack bot starting (Socket Mode)...")
|
|
247
|
+
await handler.connect_async()
|
|
248
|
+
platform_health.set_status("slack", "running")
|
|
249
|
+
try:
|
|
250
|
+
await stop_event.wait()
|
|
251
|
+
finally:
|
|
252
|
+
await handler.close_async()
|
package/src/main_telegram.py
CHANGED
|
@@ -14,7 +14,7 @@ from config import TELEGRAM_TOKEN, allowed, bot_settings, logger, save_bot_setti
|
|
|
14
14
|
from core.atomic_io import atomic_write_json, safe_load_json
|
|
15
15
|
from handlers.message_router import handle_message
|
|
16
16
|
from messengers.registry import register_adapter
|
|
17
|
-
from messengers.telegram_adapter import TelegramAdapter, markdown_to_telegram_html
|
|
17
|
+
from messengers.telegram_adapter import TelegramAdapter, markdown_to_telegram_html, safe_query_edit
|
|
18
18
|
from utils.utils import get_default_cwd
|
|
19
19
|
|
|
20
20
|
|
|
@@ -91,21 +91,45 @@ async def cmd_model(update: Update, context) -> None:
|
|
|
91
91
|
await update.message.reply_text("⚠️ No active session here. Start one with /new first.")
|
|
92
92
|
return
|
|
93
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
|
+
|
|
94
105
|
if not context.args:
|
|
95
|
-
|
|
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
|
+
)
|
|
96
127
|
return
|
|
97
128
|
|
|
98
129
|
requested = " ".join(context.args)
|
|
99
|
-
from cogs.general_cog import load_cached_models
|
|
100
|
-
|
|
101
|
-
cached_models = load_cached_models()
|
|
102
130
|
exact = next((m for m in cached_models if m.lower() == requested.lower()), None)
|
|
103
131
|
partial = next((m for m in cached_models if requested.lower() in m.lower()), None)
|
|
104
|
-
final_model = exact or partial or requested
|
|
105
|
-
|
|
106
|
-
session_manager.update_session(str(chat_id), "model", final_model)
|
|
107
|
-
bot_settings["default_model"] = final_model
|
|
108
|
-
save_bot_settings(bot_settings)
|
|
132
|
+
final_model = _apply_model(exact or partial or requested)
|
|
109
133
|
|
|
110
134
|
await adapter.send_message(
|
|
111
135
|
chat_id, f"🤖 Model changed: **{final_model}**\n💾 Also set as the default for new sessions."
|
|
@@ -114,27 +138,47 @@ async def cmd_model(update: Update, context) -> None:
|
|
|
114
138
|
|
|
115
139
|
async def cmd_credit(update: Update, context) -> None:
|
|
116
140
|
user = update.effective_user
|
|
141
|
+
adapter: TelegramAdapter = context.bot_data["adapter"]
|
|
142
|
+
|
|
117
143
|
if not allowed(user.id, "telegram"):
|
|
118
144
|
await update.message.reply_text("❌ Denied")
|
|
119
145
|
return
|
|
120
146
|
|
|
121
|
-
if not context.args or context.args[0].lower() not in ("on", "off"):
|
|
122
|
-
await update.message.reply_text("Usage: /credit <on|off>")
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
use_credits = context.args[0].lower() == "on"
|
|
126
147
|
settings_path = Path(os.getenv("HOME", "/root")) / ".gemini/antigravity-cli/settings.json"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
131
159
|
|
|
132
160
|
status_text = "🟢 **ON** (Using AI Credits)" if use_credits else "🔴 **OFF** (Using default/free model)"
|
|
133
|
-
await
|
|
134
|
-
|
|
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",
|
|
135
166
|
)
|
|
136
|
-
|
|
137
|
-
|
|
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)
|
|
138
182
|
|
|
139
183
|
|
|
140
184
|
async def on_message(update: Update, context) -> None:
|
|
@@ -192,11 +236,18 @@ async def run_telegram(stop_event: asyncio.Event) -> None:
|
|
|
192
236
|
app = build_application()
|
|
193
237
|
logger.info("✅ Telegram bot starting (polling mode)...")
|
|
194
238
|
await app.initialize()
|
|
239
|
+
if app.post_init: # not auto-called outside run_polling()/run_webhook()
|
|
240
|
+
await app.post_init(app)
|
|
195
241
|
await app.start()
|
|
196
242
|
await app.updater.start_polling(allowed_updates=Update.ALL_TYPES)
|
|
243
|
+
from core import platform_health
|
|
244
|
+
|
|
245
|
+
platform_health.set_status("telegram", "running")
|
|
197
246
|
try:
|
|
198
247
|
await stop_event.wait()
|
|
199
248
|
finally:
|
|
200
249
|
await app.updater.stop()
|
|
201
250
|
await app.stop()
|
|
202
251
|
await app.shutdown()
|
|
252
|
+
if app.post_shutdown: # same gotcha as post_init: not auto-called here
|
|
253
|
+
await app.post_shutdown(app)
|
package/src/messengers/base.py
CHANGED
|
@@ -96,6 +96,11 @@ class MessengerAdapter(ABC):
|
|
|
96
96
|
async def start_conversation(self, origin_ref: Any, title: str) -> Any:
|
|
97
97
|
raise NotImplementedError
|
|
98
98
|
|
|
99
|
+
def can_rename(self, conversation_ref: Any) -> bool:
|
|
100
|
+
"""Per-conversation version of supports_renaming - lets a single adapter answer
|
|
101
|
+
differently depending on the target (e.g. Discord threads vs. Discord DMs)."""
|
|
102
|
+
return self.supports_renaming
|
|
103
|
+
|
|
99
104
|
@abstractmethod
|
|
100
105
|
async def rename_conversation(self, conversation_ref: Any, title: str) -> None:
|
|
101
106
|
raise NotImplementedError
|
|
@@ -129,14 +129,32 @@ class DiscordAdapter(MessengerAdapter):
|
|
|
129
129
|
|
|
130
130
|
def resolve_conversation(self, conversation_id: str) -> Any:
|
|
131
131
|
try:
|
|
132
|
-
|
|
132
|
+
channel_id = int(conversation_id)
|
|
133
133
|
except (TypeError, ValueError):
|
|
134
134
|
return None
|
|
135
|
+
channel = self.bot.get_channel(channel_id)
|
|
136
|
+
if channel is not None:
|
|
137
|
+
return channel
|
|
138
|
+
# discord.py doesn't cache DM channels the way it caches guild channels/threads -
|
|
139
|
+
# DMChannel objects built from incoming messages (DMChannel._from_message) are never
|
|
140
|
+
# added to the private-channel cache, so get_channel() reliably misses for DMs. This
|
|
141
|
+
# is exactly what the approval webhook hits when it resolves a conversation purely by
|
|
142
|
+
# its stored ID (rather than from a live message/interaction object): the lookup came
|
|
143
|
+
# back None, .send() on None blew up, and the broad except in handle_approve_request
|
|
144
|
+
# swallowed it into a silent "allow" - no prompt, no tool-call display, nothing.
|
|
145
|
+
# PartialMessageable is the same fallback discord.py itself uses internally for this
|
|
146
|
+
# exact situation - it still supports send()/typing() from just the ID.
|
|
147
|
+
return discord.PartialMessageable(state=self.bot._connection, id=channel_id)
|
|
135
148
|
|
|
136
149
|
async def start_conversation(self, origin_ref: discord.Message, title: str) -> discord.Thread:
|
|
137
150
|
return await origin_ref.create_thread(name=title[:100], auto_archive_duration=1440)
|
|
138
151
|
|
|
152
|
+
def can_rename(self, conversation_ref: Any) -> bool:
|
|
153
|
+
return isinstance(conversation_ref, discord.Thread)
|
|
154
|
+
|
|
139
155
|
async def rename_conversation(self, conversation_ref: discord.Thread, title: str) -> None:
|
|
156
|
+
if not isinstance(conversation_ref, discord.Thread):
|
|
157
|
+
return # DMs (and any other non-thread channel) have no per-session title surface to rename
|
|
140
158
|
await conversation_ref.edit(name=title[:100])
|
|
141
159
|
|
|
142
160
|
@asynccontextmanager
|