Skip to content

Commit 3909d16

Browse files
committed
Add support for pin bridging
1 parent 67977d1 commit 3909d16

13 files changed

Lines changed: 542 additions & 102 deletions

File tree

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
py-cord
1+
py-cord[speed]
22
python-dotenv
33
pycryptodome
44
ujson
@@ -9,7 +9,7 @@ jellyfish
99
tld
1010
better-profanity
1111
stoat.py
12-
aiohttp
12+
aiohttp[speedups]
1313
argon2-cffi
1414
psutil
1515
git+https://github.com/greeeen-dev/capacitor.git

shinobu/beacon/cogs/frontend.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import uuid
2020
import discord
2121
from discord.ext import commands, bridge
22-
from shinobu.runtime.models import shinobu_cog
22+
from shinobu.runtime.models import shinobu_cog, ui_kit
2323
from shinobu.beacon.protocol import beacon
2424
from shinobu.beacon.models import (space as beacon_space, driver as beacon_driver, server as beacon_server,
2525
channel as beacon_channel, webhook as beacon_webhook)
26+
from shinobu.runtime.models.ui_kit import ShinobuListEntry
27+
2628

2729
class BeaconFrontend(shinobu_cog.ShinobuCog):
2830
def __init__(self, bot):
@@ -71,6 +73,43 @@ async def delete_space(self, ctx: bridge.BridgeApplicationContext | bridge.Bridg
7173
await ctx.respond(f"space deleted T.T")
7274
await self.bot.loop.run_in_executor(None, self._beacon.save_data)
7375

76+
@bridge_universal.command(name="spaces")
77+
async def list_spaces(self, ctx: bridge.BridgeApplicationContext | bridge.BridgeExtContext, space_id: str | None = None):
78+
"""Shows all available Spaces."""
79+
80+
# Get driver
81+
discord_driver: beacon_driver.BeaconDriver = self._beacon.drivers.get_driver("discord")
82+
83+
# Create new list UI
84+
list_ui: ui_kit.ShinobuListDiscordView = ui_kit.ShinobuListDiscordView(
85+
"Available Spaces",
86+
"Available Spaces",
87+
self.bot.colors.shinobu
88+
)
89+
90+
# Add spaces
91+
for space in self._beacon.spaces.all_spaces:
92+
should_hide: bool = False
93+
if space.private:
94+
# Check if we have access to this space
95+
should_hide = not space.has_access(discord_driver.get_server(str(ctx.guild.id)))
96+
97+
entry: ShinobuListEntry = ShinobuListEntry(
98+
entry_id=space.id,
99+
name=space.name,
100+
emoji=space.emoji,
101+
hidden=should_hide
102+
)
103+
entry.add_field(
104+
name="Space ID",
105+
value=f"`{space.id}`"
106+
)
107+
108+
list_ui.add_entry(entry)
109+
110+
# Run loop
111+
await list_ui.run(self.bot, ctx)
112+
74113
@bridge_universal.command(name="join-space")
75114
@commands.is_owner()
76115
async def join_space(self, ctx: bridge.BridgeApplicationContext | bridge.BridgeExtContext, space_id: str):

shinobu/beacon/discord/driver.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
312312

313313
# Process reply
314314
has_reply: bool = False
315+
is_pin: bool = content.type == beacon_message.BeaconMessageType.pins_add
316+
315317
for reply_message_group in content.replies:
316318
# Find channel-specific reply
317319
# noinspection DuplicatedCode
@@ -335,7 +337,7 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
335337
# noinspection PyTypeChecker
336338
reply_button: discord.ui.Button = discord.ui.Button(
337339
style=discord.ButtonStyle.link,
338-
label=f'Jump to message',
340+
label=f'Jump',
339341
url=reply_url
340342
)
341343

@@ -357,6 +359,11 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
357359
f"\U000021AA\U0000FE0F **Replying to @{reply_author}** \U0001F5BC\U0000FE0F"
358360
)
359361

362+
if is_pin:
363+
reply_text = discord.ui.TextDisplay(
364+
f"\U0001F4CC *Pinned a message from @{reply_author}.*"
365+
)
366+
360367
# Create reply action row
361368
reply_section: discord.ui.Section = discord.ui.Section(
362369
reply_text, accessory=reply_button
@@ -376,12 +383,15 @@ async def _to_discord_content(self, content: beacon_message.BeaconMessageContent
376383
if reply_content_trimmed:
377384
reply_button_text = f'Replying to @{reply_author} | {reply_content_trimmed}'
378385

386+
if is_pin:
387+
reply_button_text = f"Pinned a message from @{reply_author}"
388+
379389
# noinspection PyTypeChecker
380390
legacy_reply_components.add_item(discord.ui.ActionRow(
381391
discord.ui.Button(
382392
style=discord.ButtonStyle.link,
383393
label=reply_button_text,
384-
emoji='\U000021AA\U0000FE0F',
394+
emoji="\U000021AA\U0000FE0F" if not is_pin else "\U0001F4CC",
385395
url=reply_url
386396
)
387397
))
@@ -786,3 +796,17 @@ async def _purge(self, messages: list[beacon_message.BeaconMessage]):
786796

787797
# Delete messages
788798
await channel.delete_messages([discord.Object(int(message.id)) for message in messages])
799+
800+
async def _pin(self, message: beacon_message.BeaconMessage):
801+
channel = self.bot.get_channel(int(message.channel.id))
802+
partial_message: discord.PartialMessage = discord.PartialMessage(channel=channel, id=int(message.id))
803+
804+
# Pin message
805+
await partial_message.pin()
806+
807+
async def _unpin(self, message: beacon_message.BeaconMessage):
808+
channel = self.bot.get_channel(int(message.channel.id))
809+
partial_message: discord.PartialMessage = discord.PartialMessage(channel=channel, id=int(message.id))
810+
811+
# Unpin message
812+
await partial_message.unpin()

shinobu/beacon/discord/parent.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ async def _to_beacon_content(self, message: discord.Message) -> beacon_message.B
144144
results = await asyncio.gather(*tasks, return_exceptions=True)
145145
files: list[beacon_file.BeaconFile] = [result for result in results if type(result) is beacon_file.BeaconFile]
146146

147+
# Get pin status
148+
is_pin: bool = message.type == discord.MessageType.pins_add
149+
147150
# Assemble blocks
148151
blocks: dict[str, beacon_content.BeaconContentBlock] = {
149152
"content": text_content
@@ -162,7 +165,8 @@ async def _to_beacon_content(self, message: discord.Message) -> beacon_message.B
162165
files=files,
163166
replies=replies,
164167
reply_content=self._driver.sanitize_outbound(reply_content) if reply_content else None,
165-
reply_attachments=reply_attachments
168+
reply_attachments=reply_attachments,
169+
message_type=beacon_message.BeaconMessageType.pins_add if is_pin else None
166170
)
167171

168172
return content
@@ -294,11 +298,53 @@ async def handle_delete(self, message: discord.Message):
294298
except beacon.BeaconPlatformDisabled:
295299
pass
296300

301+
async def handle_pin(self, message: discord.Message):
302+
# noinspection DuplicatedCode
303+
origin_driver: beacon_driver.BeaconDriver = self._beacon.drivers.get_driver("discord")
304+
305+
# Get the BeaconMessage object for the message
306+
message_obj: beacon_message.BeaconMessage = self._beacon.messages.get_message(str(message.id))
307+
if not message_obj:
308+
# We can't pin messages that aren't cached
309+
return
310+
311+
# Convert guild data to server.BeaconServer
312+
server: beacon_server.BeaconServer = origin_driver.get_server(str(message.guild.id))
313+
314+
# Convert channel data to channel.BeaconChannel
315+
# noinspection DuplicatedCode
316+
channel: beacon_channel.BeaconChannel = origin_driver.get_channel(server, str(message.channel.id))
317+
if not channel:
318+
# We can't bridge
319+
return
320+
321+
# Get Space
322+
space: beacon_space.BeaconSpace = self._beacon.spaces.get_space_for_channel(channel)
323+
324+
if not space:
325+
# We can't bridge pins, even if it was sent in the Space by the server
326+
return
327+
328+
# Pin the message!
329+
try:
330+
await self._beacon.pin(message=message_obj, unpin=not message.pinned)
331+
except beacon.BeaconPlatformDisabled:
332+
pass
333+
297334
@commands.Cog.listener()
298335
async def on_message(self, message: discord.Message):
299336
origin_driver: beacon_driver.BeaconDriver = self._beacon.drivers.get_driver("discord")
300337

301-
# noinspection DuplicatedCode
338+
supported_types: list[discord.MessageType] = [
339+
discord.MessageType.default,
340+
discord.MessageType.reply,
341+
discord.MessageType.pins_add
342+
]
343+
supported_types_community: list[discord.MessageType] = [
344+
discord.MessageType.default,
345+
discord.MessageType.reply
346+
]
347+
302348
if message.content.startswith(self.bot.command_prefix):
303349
# Assume this is a text command
304350
return
@@ -307,6 +353,11 @@ async def on_message(self, message: discord.Message):
307353
# Do not self-bridge
308354
return
309355

356+
if message.type not in supported_types:
357+
# Unsupported message
358+
return
359+
360+
# noinspection DuplicatedCode
310361
if message.webhook_id:
311362
# Check if the webhook was ours (to prevent a self-bridge)
312363
webhook: discord.Webhook | None = origin_driver.webhooks.get_webhook(str(message.webhook_id))
@@ -386,14 +437,38 @@ async def on_message(self, message: discord.Message):
386437
pass
387438

388439
@commands.Cog.listener()
389-
async def on_message_edit(self, _, message: discord.Message):
440+
async def on_message_edit(self, before: discord.Message, message: discord.Message):
441+
# Identify update type
442+
is_pin: bool = before.pinned != message.pinned
443+
390444
# Check if message is pending
391445
if self._beacon.is_pending(str(message.id)):
392446
# Add callback
393-
self._beacon.add_callback(str(message.id), self.handle_edit, [message])
447+
if is_pin:
448+
self._beacon.add_callback(str(message.id), self.handle_pin, [message])
449+
else:
450+
self._beacon.add_callback(str(message.id), self.handle_edit, [message])
451+
else:
452+
# Run directly
453+
if is_pin:
454+
await self.handle_pin(message)
455+
else:
456+
await self.handle_edit(message)
457+
458+
@commands.Cog.listener()
459+
async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent):
460+
# Do not handle cached messages (on_message_edit does this for us)
461+
if payload.cached_message:
462+
return
463+
464+
# For uncached messages, we can only handle content edits for now. This may change in a future update
465+
# Check if message is pending
466+
if self._beacon.is_pending(str(payload.new_message.id)):
467+
# Add callback
468+
self._beacon.add_callback(str(payload.new_message.id), self.handle_edit, [payload.new_message])
394469
else:
395470
# Run directly
396-
await self.handle_edit(message)
471+
await self.handle_edit(payload.new_message)
397472

398473
@commands.Cog.listener()
399474
async def on_message_delete(self, message: discord.Message):

shinobu/beacon/fluxer/driver.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ async def _to_fluxer_content(self, content: beacon_message.BeaconMessageContent,
213213

214214
# Process reply
215215
# To not eat up too many embeds, we'll just convert the first valid reply only
216+
is_pin: bool = content.type == beacon_message.BeaconMessageType.pins_add
217+
216218
for reply_message_group in content.replies:
217219
# Find channel-specific reply
218220
# noinspection DuplicatedCode
@@ -237,13 +239,20 @@ async def _to_fluxer_content(self, content: beacon_message.BeaconMessageContent,
237239
icon_url=reply_message.author.avatar_url if reply_message.author else None
238240
)
239241

240-
# Create content text display (if possible)
241-
if reply_content:
242-
# We'll cap content to 200 characters
243-
if len(reply_content) > 200:
244-
reply_content = reply_content[:197] + "..."
245-
246-
reply_embed.description = reply_content
242+
if is_pin:
243+
reply_embed.set_author(
244+
name=f"\U0001F4CC Pinned a message from @{reply_author}",
245+
url=reply_url,
246+
icon_url=reply_message.author.avatar_url if reply_message.author else None
247+
)
248+
else:
249+
# Create content text display (if possible)
250+
if reply_content:
251+
# We'll cap content to 200 characters
252+
if len(reply_content) > 200:
253+
reply_content = reply_content[:197] + "..."
254+
255+
reply_embed.description = reply_content
247256

248257
# Append embed
249258
embeds.append(reply_embed)
@@ -511,3 +520,17 @@ async def _purge(self, messages: list[beacon_message.BeaconMessage]):
511520

512521
# Delete messages
513522
await channel.delete_messages([int(message.id) for message in messages])
523+
524+
async def _pin(self, message: beacon_message.BeaconMessage):
525+
channel: fluxer.Channel = self.bot.get_channel(int(message.channel.id))
526+
message_obj = await channel.fetch_message(int(message.id))
527+
528+
# Delete message
529+
await message_obj.pin()
530+
531+
async def _unpin(self, message: beacon_message.BeaconMessage):
532+
channel: fluxer.Channel = self.bot.get_channel(int(message.channel.id))
533+
message_obj = await channel.fetch_message(int(message.id))
534+
535+
# Delete message
536+
await message_obj.unpin()

shinobu/beacon/models/driver.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ async def _delete(self, message: beacon_message.BeaconMessage):
139139
"""Deletes a message."""
140140
raise BeaconDriverUnsupported()
141141

142+
async def _pin(self, message: beacon_message.BeaconMessage):
143+
"""Pins a message."""
144+
raise BeaconDriverUnsupported()
145+
146+
async def _unpin(self, message: beacon_message.BeaconMessage):
147+
"""Unpins a message."""
148+
raise BeaconDriverUnsupported()
149+
142150
async def _purge(self, messages: list[beacon_message.BeaconMessage]):
143151
"""Purges messages from a channel."""
144152
raise BeaconDriverUnsupported()
@@ -275,3 +283,23 @@ async def purge(self, messages: list[beacon_message.BeaconMessage]):
275283
raise BeaconDriverChannelMismatch(channel_id, message.channel.id)
276284

277285
return await self._purge(messages)
286+
287+
async def pin(self, message: beacon_message.BeaconMessage):
288+
"""Pins a message."""
289+
290+
# NOTE: You will need to overwrite BeaconDriver._pin for this to work.
291+
292+
if message.platform != self.platform:
293+
raise BeaconDriverPlatformMismatch(message.platform)
294+
295+
return await self._pin(message)
296+
297+
async def unpin(self, message: beacon_message.BeaconMessage):
298+
"""Unpins a message."""
299+
300+
# NOTE: You will need to overwrite BeaconDriver._unpin for this to work.
301+
302+
if message.platform != self.platform:
303+
raise BeaconDriverPlatformMismatch(message.platform)
304+
305+
return await self._unpin(message)

shinobu/beacon/models/message.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,25 @@
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
"""
1818

19+
from enum import Enum
1920
from shinobu.beacon.models import (content as beacon_content, abc, user as beacon_user, channel as beacon_channel,
2021
server as beacon_server, webhook as beacon_webhook, file as beacon_file,
2122
messageable as beacon_messageable)
2223

24+
class BeaconMessageType(Enum):
25+
default = 0
26+
pins_add = 1
27+
2328
class BeaconMessageContent:
2429
def __init__(self, original_id: str, original_channel_id: str, original_platform: str,
25-
blocks: dict[str, beacon_content.BeaconContentBlock],
30+
blocks: dict[str, beacon_content.BeaconContentBlock], message_type: BeaconMessageType | None = None,
2631
files: list[beacon_file.BeaconFile] | None = None, replies: list['BeaconMessageGroup'] | None = None,
2732
reply_content: str | dict[str, int] | None = None,
2833
reply_attachments: int | dict[str, int] | None = None, suppress_embeds: bool = False):
2934
self._original_id: str = original_id
3035
self._original_channel_id: str = original_channel_id
3136
self._original_platform: str = original_platform
37+
self._type: BeaconMessageType = message_type or BeaconMessageType.default
3238
self._blocks: dict[str, beacon_content.BeaconContentBlock] = blocks
3339
self._files: list[beacon_file.BeaconFile] = files or []
3440
self._replies: list[BeaconMessageGroup] = replies or []
@@ -63,6 +69,10 @@ def original_channel_id(self) -> str:
6369
def original_platform(self) -> str:
6470
return self._original_platform
6571

72+
@property
73+
def type(self) -> BeaconMessageType:
74+
return self._type
75+
6676
@property
6777
def blocks(self) -> dict[str, beacon_content.BeaconContentBlock]:
6878
return self._blocks

0 commit comments

Comments
 (0)