Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ The required scopes are:
And the required permissions:
- Embed Links
- Manage Messages
- Manage Webhooks
- Read Message History
- Send Messages
- Send Messages in Threads
Expand Down
90 changes: 82 additions & 8 deletions cogs/link_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,15 @@ async def _format_link_data(link: WebsiteLink, original_message: discore.Message
async def fix_embeds(
original_message: discore.Message,
guild: Guild,
links: List[WebsiteLink]) -> None:
links: List[WebsiteLink],
bot: discore.Client) -> None:
"""
Edit the message if necessary, and send the fixed links.

:param original_message: the message to fix
:param guild: the guild associated with the context
:param links: the WebsiteLink objects to fix
:param bot: the bot sending the fixed links

Remark:
Discord API, when sending a message with links, first successfully sends
Expand All @@ -136,11 +138,17 @@ async def fix_embeds(
or (isinstance(channel, discore.Thread) and (channel.locked or channel.archived))):
return

async with Typing(channel):
async def render_and_send() -> tuple[list[tuple[str, list[WebsiteLink]]], dict[discore.Message, list[WebsiteLink]]]:
rendered_links = [link for link in links if await link.render()]
if not rendered_links:
return
not_sent, messages = await send_fixed_links(rendered_links, guild, original_message)
return [], {}
return await send_fixed_links(rendered_links, guild, original_message, bot)

if guild.reply_as_original_author_replica:
not_sent, messages = await render_and_send()
else:
async with Typing(channel):
not_sent, messages = await render_and_send()

to_delete = []
if messages:
Expand Down Expand Up @@ -173,7 +181,8 @@ async def fix_embeds(
async def send_fixed_links(
rendered_links: list[WebsiteLink],
guild: Guild,
original_message: discore.Message
original_message: discore.Message,
bot: discore.Client
) -> tuple[list[tuple[str, list[WebsiteLink]]], dict[discore.Message, list[WebsiteLink]]]:
"""
Send the fixed links to the channel, according to the guild settings and its context
Expand All @@ -193,20 +202,25 @@ async def send_fixed_links(
:param rendered_links: the rendered WebsiteLink objects to send
:param guild: the guild associated with the context
:param original_message: the original message associated with the context to reply to
:param bot: the bot sending the fixed links
:return: a tuple containing the list of links that failed to be sent, and a dict of the messages sent with their corresponding links
"""

messages_sent: dict[discore.Message, list[WebsiteLink]] = {}
links_failed: list[tuple[str, list[WebsiteLink]]] = []

grouped = group_items(rendered_links, 2000)
use_original_author_replica = guild.reply_as_original_author_replica
webhook = await get_or_create_webhook(original_message.channel, bot) if use_original_author_replica else None

for i, (message_content, links_in_group) in enumerate(grouped):
if i == 0 and guild.reply_to_message:
if webhook is not None:
coro = webhook_send(webhook, original_message, message_content, guild.reply_silently)
elif i == 0 and guild.reply_to_message:
coro = discore.fallback_reply(original_message, message_content, silent=guild.reply_silently)
else:
coro = original_message.channel.send(message_content, silent=guild.reply_silently)

sent, msg = await safe_send_coro(coro, invalid_form_body='Embed size exceeds maximum size', forbidden=True)
if sent and msg:
messages_sent[msg] = links_in_group
Expand All @@ -216,6 +230,66 @@ async def send_fixed_links(
return links_failed, messages_sent


async def get_or_create_webhook(channel: GuildMessageableChannel, bot: discore.Client) -> discore.Webhook | None:
"""
Get or create the webhook used to send messages as the original author.

:param channel: the channel to send the fixed links to
:param bot: the bot creating or retrieving the webhook
:return: the webhook to use, if available
"""

webhook_channel = channel.parent if isinstance(channel, discore.Thread) else channel
if webhook_channel is None:
return None

if not hasattr(webhook_channel, 'webhooks') or not hasattr(webhook_channel, 'create_webhook'):
return None

if not webhook_channel.permissions_for(channel.guild.me).manage_webhooks:
return None

success, webhooks = await safe_send_coro(webhook_channel.webhooks(), forbidden=True)
if not success:
return None
webhook = next((
w for w in webhooks
if getattr(w.user, 'id', None) == bot.user.id
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should have told me that self wasn't available in this context instead of passing the reference across the whole file, my bad. You can use discore.Bot.get() in this context.

), None)
if webhook is not None:
return webhook
success, webhook = await safe_send_coro(webhook_channel.create_webhook(name=bot.user.display_name), forbidden=True)
return webhook if success else None


async def webhook_send(
webhook: discore.Webhook,
original_message: discore.Message,
content: str,
silent: bool
) -> discore.Message:
"""
Send a fixed link using the original author's display name and avatar.

:param webhook: the webhook to use
:param original_message: the message associated with the context to reply to
:param content: the content to send
:param silent: whether to send the message silently
:return: the message created by the webhook
"""

kwargs = {
'content': content,
'username': original_message.author.display_name,
'avatar_url': original_message.author.display_avatar.url,
'silent': silent,
'wait': True
}
if isinstance(original_message.channel, discore.Thread):
kwargs['thread'] = original_message.channel
return await webhook.send(**kwargs)


async def wait_for_embed(message: discore.Message) -> bool:
"""
Wait for the message to have embeds.
Expand Down Expand Up @@ -302,4 +376,4 @@ async def on_message(self, message: discore.Message) -> None:
if message.webhook_id is not None and not bool(guild.webhooks):
return

await fix_embeds(message, guild, links)
await fix_embeds(message, guild, links, self.bot)
Comment thread
Kyrela marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""AddOriginalAuthorReplicaReplies Migration."""

from masoniteorm.migrations import Migration


class AddOriginalAuthorReplicaReplies(Migration):
def up(self):
"""
Run the migrations.
"""
with self.schema.table("guilds") as table:
table.boolean("reply_as_original_author_replica").after("reply_silently").default(False)

def down(self):
"""
Revert the migrations.
"""
with self.schema.table("guilds") as table:
table.drop_column("reply_as_original_author_replica")
1 change: 1 addition & 0 deletions database/models/Guild.py
Comment thread
Kyrela marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Guild(DiscordRepresentation):
'roles_use_any_rule': bool,
'reply_to_message': bool,
'reply_silently': bool,
'reply_as_original_author_replica': bool,
'webhooks': bool,
'original_message': OriginalMessage,
'twitter_view': FxEmbedView,
Expand Down
12 changes: 11 additions & 1 deletion locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ settings:
read_message_history:
"true": "🟢 `Read message history` permission"
"false": "🔴 Missing `read message history` permission"
manage_webhooks:
"true": "🟢 `Manage webhooks` permission"
"false": "🔴 Missing `manage webhooks` permission"
filters:
button:
toggle:
Expand Down Expand Up @@ -148,7 +151,7 @@ settings:
reply_method:
name: "Reply method"
description: "Change the behavior on the reply"
content: "**Change what to do on the reply**\n- %{state}\n- %{silent}%{perms}"
content: "**Change what to do on the reply**\n- %{state}\n- %{silent}\n- %{replica}%{perms}"
reply:
button:
"true": "Replying"
Expand All @@ -163,6 +166,13 @@ settings:
state:
"true": "🔕 Send silently"
"false": "🔔 Send with a notification"
original_author_replica:
button:
"true": "Send as original author replica"
"false": "Send as %{bot}"
state:
"true": "🪪 Send as original author replica"
"false": "🤖 Send as %{bot}"
webhooks:
name: "Webhooks"
description: "Enable/Disable for webhooks"
Expand Down
12 changes: 11 additions & 1 deletion locales/ko.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ settings:
read_message_history:
"true": "🟢 `메시지 기록 보기` 권한"
"false": "🔴 `메시지 기록 보기` 권한 없음"
manage_webhooks:
"true": "🟢 `웹후크 관리하기` 권한"
"false": "🔴 `웹후크 관리하기` 권한 없음"
filters:
button:
toggle:
Expand Down Expand Up @@ -145,7 +148,7 @@ settings:
reply_method:
name: "답장 방법"
description: "답장할 때의 동작 변경하기"
content: "**답장할 때 수행할 작업 변경하기**\n- %{state}\n- %{silent}%{perms}"
content: "**답장할 때 수행할 작업 변경하기**\n- %{state}\n- %{silent}\n- %{replica}%{perms}"
reply:
button:
"true": "답장"
Expand All @@ -160,6 +163,13 @@ settings:
state:
"true": "🔕 조용히 보내기"
"false": "🔔 알림과 함께 보내기"
original_author_replica:
button:
"true": "원본 작성자 복제본으로 보내기"
"false": "%{bot}(으)로 보내기"
state:
"true": "🪪 원본 작성자 복제본으로 보내기"
"false": "🤖 %{bot}(으)로 보내기"
webhooks:
name: "웹후크"
description: "웹후크 활성화/비활성화하기"
Expand Down
42 changes: 38 additions & 4 deletions src/settings.py
Comment thread
Kyrela marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ async def embed(self) -> discore.Embed:
perms.append('manage_messages')
if self.ctx.guild.reply_to_message:
perms.append('read_message_history')
if self.ctx.guild.reply_as_original_author_replica:
perms.append('manage_webhooks')
embed.add_field(
name=t('settings.troubleshooting.permissions', channel=self.ctx.channel.mention),
value=format_perms(perms, self.ctx.channel.discord_object, include_label=False, include_valid=True),
Expand Down Expand Up @@ -1123,7 +1125,8 @@ class ReplyMethodSetting(BaseSetting):

def __init__(self, interaction: discore.Interaction, view: SettingsView, ctx: DataElements):
super().__init__(interaction, view, ctx)
self.reply_to_message = bool(ctx.guild.reply_to_message)
self.reply_as_original_author_replica = bool(ctx.guild.reply_as_original_author_replica)
self.reply_to_message = bool(ctx.guild.reply_to_message and not self.reply_as_original_author_replica)
self.reply_silently = bool(ctx.guild.reply_silently)

@property
Expand All @@ -1137,21 +1140,31 @@ async def embed(self) -> discore.Embed:
perms.append('send_messages_in_threads')
if self.reply_to_message:
perms.append('read_message_history')
if self.reply_as_original_author_replica:
perms.append('manage_webhooks')
embed = discore.Embed(
title=f"{self.emoji} {t(self.name)}",
description=t(
'settings.reply_method.content',
state=t(f'settings.reply_method.reply.state.{l(self.reply_to_message)}', emoji=self.emoji),
silent=t(f'settings.reply_method.silent.state.{l(self.reply_silently)}'),
replica=t(
f'settings.reply_method.original_author_replica.state.{l(self.reply_as_original_author_replica)}',
bot=self.bot.user.display_name),
perms=format_perms(perms, self.ctx.channel.discord_object))
)
discore.set_embed_footer(self.bot, embed)
return embed

@property
async def option(self) -> discore.SelectOption:
has_missing_perms = (
(self.reply_to_message and is_missing_perm(['read_message_history'], self.ctx.channel.discord_object))
or (self.reply_as_original_author_replica
and is_missing_perm(['manage_webhooks'], self.ctx.channel.discord_object))
)
return discore.SelectOption(
label=('⚠️ ' if self.reply_to_message and is_missing_perm(['read_message_history'], self.ctx.channel.discord_object) else '')
label=('⚠️ ' if has_missing_perms else '')
+ t(self.name),
value=self.id,
description=t(self.description),
Expand All @@ -1163,7 +1176,8 @@ async def items(self) -> List[discore.ui.Item]:
reply_to_message_button = discore.ui.Button(
style=discore.ButtonStyle.primary if self.reply_to_message else discore.ButtonStyle.secondary,
label=t(f'settings.reply_method.reply.button.{l(self.reply_to_message)}'),
custom_id=self.id
custom_id=self.id,
disabled=self.reply_as_original_author_replica
)
edit_callback(reply_to_message_button, self.view, self.toggle_reply_to_message)
reply_silently_button = discore.ui.Button(
Expand All @@ -1172,9 +1186,19 @@ async def items(self) -> List[discore.ui.Item]:
custom_id='reply_silently'
)
edit_callback(reply_silently_button, self.view, self.toggle_reply_silently)
return [reply_to_message_button, reply_silently_button]
original_author_replica_button = discore.ui.Button(
style=discore.ButtonStyle.primary if self.reply_as_original_author_replica else discore.ButtonStyle.secondary,
label=t(
f'settings.reply_method.original_author_replica.button.{l(self.reply_as_original_author_replica)}',
bot=self.bot.user.display_name),
custom_id='reply_as_original_author_replica'
)
edit_callback(original_author_replica_button, self.view, self.toggle_reply_as_original_author_replica)
return [reply_to_message_button, reply_silently_button, original_author_replica_button]

async def toggle_reply_to_message(self, view: SettingsView, interaction: discore.Interaction, _) -> None:
if self.reply_as_original_author_replica:
return
self.reply_to_message = not self.reply_to_message
self.ctx.guild.update({'reply_to_message': self.reply_to_message})
await view.refresh(interaction)
Expand All @@ -1184,6 +1208,16 @@ async def toggle_reply_silently(self, view: SettingsView, interaction: discore.I
self.ctx.guild.update({'reply_silently': self.reply_silently})
await view.refresh(interaction)

async def toggle_reply_as_original_author_replica(self, view: SettingsView, interaction: discore.Interaction, _) -> None:
self.reply_as_original_author_replica = not self.reply_as_original_author_replica
if self.reply_as_original_author_replica:
self.reply_to_message = False
self.ctx.guild.update({
'reply_as_original_author_replica': self.reply_as_original_author_replica,
'reply_to_message': self.reply_to_message
})
await view.refresh(interaction)


class WebhooksSetting(BaseSetting):
"""Represents the webhooks setting (respond to webhooks or not)"""
Expand Down
Loading