Skip to content

Commit 28f9c60

Browse files
committed
Fix issues with Discord bridge (it works now!!)
1 parent 4e6f75b commit 28f9c60

12 files changed

Lines changed: 327 additions & 83 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ that's also flexible and ready for the demands of tomorrow.
1919

2020
## Todo
2121
- [X] Shinobu Runtime (core, secrets manager, debug tools, etc)
22-
- [ ] Basic Discord cross-server bridge (v0.1)
22+
- [X] Basic Discord cross-server bridge (v0.1)
2323
- [ ] Stoat (formerly Revolt) support (v0.2)
2424
- [ ] Fluxer support (v0.2/v0.3)
2525
- [ ] Moderation & customization tools (v0.3/v1)

run.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Shinobu - Converse from anywhere, anytime.
3+
Copyright (C) 2026-present Green (@greeeen-dev)
4+
5+
This program is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU Affero General Public License as
7+
published by the Free Software Foundation, either version 3 of the
8+
License, or (at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU Affero General Public License for more details.
14+
15+
You should have received a copy of the GNU Affero General Public License
16+
along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
"""
18+
19+
# An intermediate script for running Shinobu with the configured Python install or venv.
20+
21+

run.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
FILEPATH="$(which python3)"
4+
5+
if [[ -z $FILEPATH ]]; then
6+
echo "Could not find a Python 3 installation."
7+
exit 1
8+
fi
9+
10+
python3 run.py

shinobu/beacon/cogs/backend.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
"""
1818

19+
from discord.ext import commands
1920
from shinobu.runtime.models import shinobu_cog
2021
from shinobu.beacon.protocol import beacon
2122

@@ -41,6 +42,11 @@ def on_entitlements_issued(self):
4142
self._beacon = beacon.Beacon(self.bot, self._shinobu_files)
4243
self.bot.shared_objects.add("beacon", self._beacon)
4344

45+
@commands.Cog.listener()
46+
async def on_ready(self):
47+
if not self._beacon.initialized:
48+
await self._beacon.load_data()
49+
4450
def get_cog_type():
4551
return BeaconBackend
4652

shinobu/beacon/cogs/frontend.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ async def new_space(self, ctx: commands.Context, name: str):
5252
space_name=name
5353
)
5454
self._beacon.spaces.add_space(new_space)
55-
await ctx.send(f"space created!\n- id: `{new_space.id}`\n-name: {new_space.name}")
55+
await ctx.send(f"space created!\n- id: `{new_space.id}`\n- name: {new_space.name}")
56+
await self.bot.loop.run_in_executor(None, self._beacon.save_data)
5657

5758
@bridge_text.command(name="join-space")
5859
@commands.is_owner()
@@ -92,6 +93,7 @@ async def join_space(self, ctx: commands.Context, space_id: str):
9293
return await ctx.send("already in space? :/")
9394

9495
await ctx.send("space joined! :3")
96+
await self.bot.loop.run_in_executor(None, self._beacon.save_data)
9597

9698
def get_cog_type():
9799
return BeaconFrontend

shinobu/beacon/discord/driver.py

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@
2727
content as beacon_content, file as beacon_file)
2828

2929
class DiscordMessageContent:
30-
def __init__(self, content: str | None = None, components: discord.ui.BaseView | discord.ui.View | None = None,
30+
def __init__(self, content: str | None = None, components: discord.ui.DesignerView | discord.ui.View | None = None,
3131
files: list[discord.File] | None = None, embeds: list[discord.Embed] | None = None):
3232
self._content: str | None = content
33-
self._components: discord.ui.BaseView | discord.ui.View | None = components
33+
self._components: discord.ui.DesignerView | discord.ui.View | None = components
3434
self._files: list[discord.File] = files or []
3535
self._embeds: list[discord.Embed] = embeds or []
3636
self._components_v2: bool = (
37-
type(components) is discord.ui.BaseView and components.is_components_v2()
37+
type(components) is discord.ui.DesignerView and components.is_components_v2()
3838
) if components else False
3939

4040
@property
@@ -49,7 +49,7 @@ def raw_content(self) -> str | None:
4949
return self._content
5050

5151
@property
52-
def components(self) -> discord.ui.BaseView | discord.ui.View | None:
52+
def components(self) -> discord.ui.DesignerView | discord.ui.View | None:
5353
return self._components
5454

5555
@property
@@ -125,8 +125,6 @@ def embed_container(block: beacon_content.BeaconContentEmbed) -> discord.ui.Cont
125125
container.add_text(author_content)
126126

127127
# Add title and description block
128-
title_thumbnail_row = discord.ui.ActionRow()
129-
130128
title_desc_content = f"## {block.title}\n{block.description}"
131129

132130
if block.url:
@@ -136,14 +134,17 @@ def embed_container(block: beacon_content.BeaconContentEmbed) -> discord.ui.Cont
136134
title_desc_content = block.description
137135

138136
if title_desc_content:
139-
title_thumbnail_row.add_item(discord.ui.TextDisplay(title_desc_content))
137+
title_thumbnail_section = discord.ui.Section()
138+
title_thumbnail_section.add_text(title_desc_content)
139+
140+
# Add thumbnail
141+
if block.thumbnail:
142+
title_thumbnail_section.set_accessory(discord.ui.Thumbnail(block.thumbnail))
140143

141-
# Add thumbnail
142-
if block.thumbnail:
143-
title_thumbnail_row.add_item(discord.ui.Thumbnail(block.thumbnail))
144+
container.add_item(title_thumbnail_section)
144145

145146
# Add fields
146-
current_row = discord.ui.ActionRow()
147+
current_section = discord.ui.Section()
147148
for field in block.fields:
148149
field_components = []
149150

@@ -152,15 +153,15 @@ def embed_container(block: beacon_content.BeaconContentEmbed) -> discord.ui.Cont
152153
if field["value"]:
153154
field_components.append(field["value"])
154155

155-
current_row.add_item(discord.ui.TextDisplay("\n".join(field_components)))
156+
current_section.add_text("\n".join(field_components))
156157

157-
if not field["inline"] or len(current_row.items) == 3:
158-
container.add_row(current_row)
159-
current_row = discord.ui.ActionRow()
158+
if not field["inline"] or len(current_section.items) == 3:
159+
container.add_item(current_section)
160+
current_section = discord.ui.Section()
160161

161162
# Ensure all rows have been added
162-
if len(current_row.items) > 0:
163-
container.add_row(current_row)
163+
if len(current_section.items) > 0:
164+
container.add_item(current_section)
164165

165166
# Add media
166167
if block.media:
@@ -174,7 +175,8 @@ def embed_container(block: beacon_content.BeaconContentEmbed) -> discord.ui.Cont
174175
if block.timestamp:
175176
footer_components.append(f"<t:{round(block.timestamp)}:f>")
176177

177-
current_row.add_item(discord.ui.TextDisplay(" • ".join(footer_components)))
178+
if len(footer_components) > 0:
179+
container.add_item(discord.ui.TextDisplay(" • ".join(footer_components)))
178180

179181
return container
180182

@@ -271,8 +273,9 @@ def _to_beacon_webhook(self, webhook: discord.Webhook) -> beacon_webhook.BeaconW
271273
channel=channel
272274
)
273275

274-
async def _to_discord_content(self, content: beacon_message.BeaconMessageContent, use_components_v2: bool | None = None
275-
) -> DiscordMessageContent:
276+
async def _to_discord_content(self, content: beacon_message.BeaconMessageContent,
277+
destination: beacon_messageable.BeaconMessageable, use_components_v2: bool | None = None
278+
) -> DiscordMessageContent:
276279
if use_components_v2 is None:
277280
use_components_v2 = self._use_components_v2
278281

@@ -299,7 +302,13 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
299302
legacy_embeds.append(DiscordBeaconContentBlockConverter.embed(block_obj))
300303

301304
# Process reply
302-
for reply_message in content.replies:
305+
for reply_message_group in content.replies:
306+
# Find channel-specific reply
307+
reply_message: beacon_message.BeaconMessage | None = reply_message_group.get_message_for(destination)
308+
309+
if not reply_message:
310+
continue
311+
303312
reply_author: str = f"{reply_message.author.display_name if reply_message.author else '[unknown]'}"
304313
reply_url: str = f"https://discord.com/channels/{reply_message.server.id}/{reply_message.channel.id}/{reply_message.id}"
305314
reply_content: str | None = None
@@ -327,28 +336,32 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
327336
# Create reply container (will get ID 10X)
328337
reply_container: discord.ui.Container = discord.ui.Container()
329338

330-
# Create list for reply items
339+
# Create reply jump button
331340
# noinspection PyTypeChecker
332-
reply_items: list[discord.ui.Item] = [
333-
discord.ui.Button(
334-
style=discord.ButtonStyle.link,
335-
label=f'Replying to @{reply_author}',
336-
emoji='\U000021AA\U0000FE0F',
337-
url=reply_url
338-
)
339-
]
341+
reply_button: discord.ui.Button = discord.ui.Button(
342+
style=discord.ButtonStyle.link,
343+
label=f'Jump to message',
344+
url=reply_url
345+
)
346+
reply_text: discord.ui.TextDisplay = discord.ui.TextDisplay(
347+
f"\U000021AA\U0000FE0F **Replying to @{reply_author}**"
348+
)
340349

341350
# Create content text display (if possible)
342351
if reply_content:
343352
# We'll cap content to 200 characters
344353
if len(reply_content) > 200:
345354
reply_content = reply_content[:197] + "..."
346355

347-
reply_items.append(discord.ui.TextDisplay(reply_content))
356+
reply_text= discord.ui.TextDisplay(
357+
f"\U000021AA\U0000FE0F **Replying to @{reply_author}** - {reply_content}"
358+
)
348359

349360
# Create reply action row
350-
reply_row: discord.ui.ActionRow = discord.ui.ActionRow(*reply_items)
351-
reply_container.add_row(reply_row)
361+
reply_section: discord.ui.Section = discord.ui.Section(
362+
reply_text, accessory=reply_button
363+
)
364+
reply_container.add_item(reply_section)
352365
reply_blocks.append(reply_container)
353366

354367
# Add button to legacy reply components
@@ -370,7 +383,7 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
370383
# Assemble to DiscordMessageContent
371384
if use_components_v2:
372385
# Assemble components
373-
components = discord.ui.BaseView(
386+
components = discord.ui.DesignerView(
374387
store=False
375388
)
376389

@@ -513,7 +526,7 @@ async def fetch_webhook(self, webhook_id: str):
513526

514527
async def send(self, destination: beacon_messageable.BeaconMessageable,
515528
content: beacon_message.BeaconMessageContent, send_as: beacon_user.BeaconUser | None = None,
516-
webhook_id: str | None = None):
529+
webhook_id: str | None = None, self_send: bool = False):
517530
# Get message options
518531
send_as_webhook: bool = webhook_id is not None
519532
send_as_user: bool = send_as is not None
@@ -540,12 +553,42 @@ async def send(self, destination: beacon_messageable.BeaconMessageable,
540553

541554
# Convert message content data
542555
discord_content: DiscordMessageContent = await self._to_discord_content(
543-
content, use_components_v2=self._use_components_v2
556+
content, destination, use_components_v2=self._use_components_v2
544557
)
545558

546-
# Send the message!
559+
# Convert bot user to BeaconUser
560+
self_user = self.get_user(str(self.bot.user.id))
561+
562+
# Get target
547563
if webhook_id:
548564
target: discord.TextChannel | discord.abc.Messageable = webhook_obj.channel
565+
else:
566+
target: discord.TextChannel | discord.abc.Messageable = self.bot.get_channel(int(destination.id))
567+
568+
# Convert channel to BeaconChannel
569+
channel: beacon_channel.BeaconChannel = self.get_channel(self.get_server(str(target.guild.id)), str(target.id))
570+
571+
# Are we self-sending?
572+
if str(webhook_obj.channel_id) == content.original_channel_id and not self_send:
573+
# Return message object but don't send
574+
575+
return beacon_message.BeaconMessage(
576+
message_id=content.original_id,
577+
platform=self.platform,
578+
author=send_as or self_user,
579+
server=self.get_server(str(target.guild.id)),
580+
channel=channel,
581+
content=discord_content.raw_content,
582+
attachments=len(discord_content.files),
583+
replies=[reply.get_message_for(channel) for reply in content.replies] if channel else [],
584+
webhook_id=webhook_id if webhook_obj else None
585+
)
586+
587+
# Send the message!
588+
if webhook_id:
589+
if not target:
590+
return None
591+
549592
message = await webhook_obj.send(
550593
content=discord_content.content,
551594
view=discord_content.components,
@@ -556,8 +599,6 @@ async def send(self, destination: beacon_messageable.BeaconMessageable,
556599
wait=True
557600
)
558601
else:
559-
target: discord.TextChannel | discord.abc.Messageable = self.bot.get_channel(int(destination.id))
560-
561602
if not target:
562603
return None
563604

@@ -568,18 +609,15 @@ async def send(self, destination: beacon_messageable.BeaconMessageable,
568609
files=discord_content.files
569610
)
570611

571-
# Convert message to BeaconMessage
572-
self_user = self.get_user(str(self.bot.user.id))
573-
574612
return beacon_message.BeaconMessage(
575613
message_id=str(message.id),
576614
platform=self.platform,
577615
author=send_as or self_user,
578616
server=self.get_server(str(target.guild.id)),
579-
channel=self.get_channel(self.get_server(str(target.guild.id)), str(target.id)),
617+
channel=channel,
580618
content=discord_content.raw_content,
581619
attachments=len(discord_content.files),
582-
replies=content.replies,
620+
replies=[reply.get_message_for(channel) for reply in content.replies] if channel else [],
583621
webhook_id=webhook_id if webhook_obj else None
584622
)
585623

@@ -589,7 +627,7 @@ async def _edit(self, message: beacon_message.BeaconMessage, content: beacon_mes
589627

590628
# Convert message content data
591629
discord_content: DiscordMessageContent = await self._to_discord_content(
592-
content, use_components_v2=self._use_components_v2
630+
content, destination=message.channel, use_components_v2=self._use_components_v2
593631
)
594632

595633
# Edit message

shinobu/beacon/discord/parent.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ async def _to_beacon_content(self, message: discord.Message) -> beacon_message.B
136136
embeds_index += 1
137137

138138
content: beacon_message.BeaconMessageContent = beacon_message.BeaconMessageContent(
139+
original_id=str(message.id),
140+
original_channel_id=str(message.channel.id),
139141
blocks=blocks,
140142
files=files,
141143
replies=replies
@@ -159,6 +161,10 @@ async def _get_attachment_data(attachment: discord.Attachment) -> beacon_file.Be
159161
async def on_message(self, message: discord.Message):
160162
origin_driver: beacon_driver.BeaconDriver = self._beacon.drivers.get_driver("discord")
161163

164+
if message.content.startswith(self.bot.command_prefix):
165+
# Assume this is a text command
166+
return
167+
162168
if message.author.id == self.bot.user.id:
163169
# Do not self-bridge
164170
return
@@ -194,6 +200,10 @@ async def on_message(self, message: discord.Message):
194200
# Get Space
195201
space: beacon_space.BeaconSpace = self._beacon.spaces.get_space_for_channel(channel)
196202

203+
# Get the ID of the webhook to use
204+
membership: beacon_space.BeaconSpaceMember = space.get_member(server)
205+
webhook_id = membership.webhook_id
206+
197207
if not space:
198208
# We can't bridge
199209
return
@@ -203,18 +213,20 @@ async def on_message(self, message: discord.Message):
203213
author=author,
204214
space=space,
205215
content=content,
206-
webhook_id=str(message.webhook_id),
216+
webhook_id=webhook_id,
207217
skip_filter=True
208218
)
209219

220+
# TODO: Add returning the block reason.
210221
if preliminary_block:
211222
return
212223

213224
# Send message!
214225
await self._beacon.send(
215226
author=author,
216227
space=space,
217-
content=content
228+
content=content,
229+
webhook_id=webhook_id
218230
)
219231

220232
def get_cog_type():

0 commit comments

Comments
 (0)