Skip to content

Commit d9b26b1

Browse files
committed
Add original sender profile reply mode
1 parent 4916802 commit d9b26b1

6 files changed

Lines changed: 119 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ The required scopes are:
168168
And the required permissions:
169169
- Embed Links
170170
- Manage Messages
171+
- Manage Webhooks
171172
- Read Message History
172173
- Send Messages
173174
- Send Messages in Threads

cogs/link_fix.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
__all__ = ('LinkFix',)
2222

2323
_logger = logging.getLogger(__name__)
24+
WEBHOOK_NAME = "FixTweetBot"
2425

2526

2627
def get_website(guild: Guild, url: str, spoiler: bool = False) -> WebsiteLink | None:
@@ -136,7 +137,13 @@ async def fix_embeds(
136137
or (isinstance(channel, discore.Thread) and (channel.locked or channel.archived))):
137138
return
138139

139-
async with Typing(channel):
140+
if not guild.reply_as_original_sender_profile:
141+
async with Typing(channel):
142+
rendered_links = [link for link in links if await link.render()]
143+
if not rendered_links:
144+
return
145+
not_sent, messages = await send_fixed_links(rendered_links, guild, original_message)
146+
else:
140147
rendered_links = [link for link in links if await link.render()]
141148
if not rendered_links:
142149
return
@@ -200,9 +207,15 @@ async def send_fixed_links(
200207
links_failed: list[tuple[str, list[WebsiteLink]]] = []
201208

202209
grouped = group_items(rendered_links, 2000)
210+
use_original_sender_profile = guild.reply_as_original_sender_profile
211+
webhook = await get_or_create_webhook(original_message.channel) if use_original_sender_profile else None
212+
if use_original_sender_profile and webhook is None:
213+
return [(message_content, links_in_group) for message_content, links_in_group in grouped], messages_sent
203214

204215
for i, (message_content, links_in_group) in enumerate(grouped):
205-
if i == 0 and guild.reply_to_message:
216+
if webhook is not None:
217+
coro = webhook_send(webhook, original_message, message_content, guild.reply_silently)
218+
elif i == 0 and guild.reply_to_message:
206219
coro = discore.fallback_reply(original_message, message_content, silent=guild.reply_silently)
207220
else:
208221
coro = original_message.channel.send(message_content, silent=guild.reply_silently)
@@ -216,6 +229,64 @@ async def send_fixed_links(
216229
return links_failed, messages_sent
217230

218231

232+
async def get_or_create_webhook(channel: discore.TextChannel | discore.Thread) -> discore.Webhook | None:
233+
"""
234+
Get or create the webhook used to send messages as the original sender.
235+
236+
:param channel: the channel to send the fixed links to
237+
:return: the webhook to use, if available
238+
"""
239+
240+
webhook_channel = channel.parent if isinstance(channel, discore.Thread) else channel
241+
if webhook_channel is None:
242+
return None
243+
244+
if not webhook_channel.permissions_for(channel.guild.me).manage_webhooks:
245+
return None
246+
247+
try:
248+
webhooks = await webhook_channel.webhooks()
249+
except discore.HTTPException:
250+
_logger.exception("Failed to fetch webhooks")
251+
return None
252+
webhook = next((
253+
w for w in webhooks
254+
if w.name == WEBHOOK_NAME and getattr(w.user, 'id', None) == channel.guild.me.id
255+
), None)
256+
if webhook is not None:
257+
return webhook
258+
sent, webhook = await safe_send_coro(webhook_channel.create_webhook(name=WEBHOOK_NAME), forbidden=True)
259+
return webhook if sent else None
260+
261+
262+
async def webhook_send(
263+
webhook: discore.Webhook,
264+
original_message: discore.Message,
265+
content: str,
266+
silent: bool
267+
) -> discore.Message:
268+
"""
269+
Send a fixed link using the original sender's display name and avatar.
270+
271+
:param webhook: the webhook to use
272+
:param original_message: the message associated with the context to reply to
273+
:param content: the content to send
274+
:param silent: whether to send the message silently
275+
:return: the message created by the webhook
276+
"""
277+
278+
kwargs = {
279+
'content': content,
280+
'username': original_message.author.display_name,
281+
'avatar_url': original_message.author.display_avatar.url,
282+
'silent': silent,
283+
'wait': True
284+
}
285+
if isinstance(original_message.channel, discore.Thread):
286+
kwargs['thread'] = original_message.channel
287+
return await webhook.send(**kwargs)
288+
289+
219290
async def wait_for_embed(message: discore.Message) -> bool:
220291
"""
221292
Wait for the message to have embeds.

database/models/Guild.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class Guild(DiscordRepresentation):
6161
'roles_use_any_rule': bool,
6262
'reply_to_message': bool,
6363
'reply_silently': bool,
64+
'reply_as_original_sender_profile': bool,
6465
'webhooks': bool,
6566
'original_message': OriginalMessage,
6667
'twitter_view': FxEmbedView,

locales/en-US.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ settings:
5656
read_message_history:
5757
"true": "🟢 `Read message history` permission"
5858
"false": "🔴 Missing `read message history` permission"
59+
manage_webhooks:
60+
"true": "🟢 `Manage webhooks` permission"
61+
"false": "🔴 Missing `manage webhooks` permission"
5962
filters:
6063
button:
6164
toggle:
@@ -148,7 +151,7 @@ settings:
148151
reply_method:
149152
name: "Reply method"
150153
description: "Change the behavior on the reply"
151-
content: "**Change what to do on the reply**\n- %{state}\n- %{silent}%{perms}"
154+
content: "**Change what to do on the reply**\n- %{state}\n- %{silent}\n- %{avatar}%{perms}"
152155
reply:
153156
button:
154157
"true": "Replying"
@@ -163,6 +166,13 @@ settings:
163166
state:
164167
"true": "🔕 Send silently"
165168
"false": "🔔 Send with a notification"
169+
original_sender_profile:
170+
button:
171+
"true": "Send as Original Sender Profile"
172+
"false": "Send as Bot Profile"
173+
state:
174+
"true": "🪪 Send as Original Sender Profile"
175+
"false": "🤖 Send as Bot Profile"
166176
webhooks:
167177
name: "Webhooks"
168178
description: "Enable/Disable for webhooks"

locales/ko.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ settings:
5353
read_message_history:
5454
"true": "🟢 `메시지 기록 보기` 권한"
5555
"false": "🔴 `메시지 기록 보기` 권한 없음"
56+
manage_webhooks:
57+
"true": "🟢 `웹후크 관리하기` 권한"
58+
"false": "🔴 `웹후크 관리하기` 권한 없음"
5659
filters:
5760
button:
5861
toggle:
@@ -145,7 +148,7 @@ settings:
145148
reply_method:
146149
name: "답장 방법"
147150
description: "답장할 때의 동작 변경하기"
148-
content: "**답장할 때 수행할 작업 변경하기**\n- %{state}\n- %{silent}%{perms}"
151+
content: "**답장할 때 수행할 작업 변경하기**\n- %{state}\n- %{silent}\n- %{avatar}%{perms}"
149152
reply:
150153
button:
151154
"true": "답장"
@@ -160,6 +163,13 @@ settings:
160163
state:
161164
"true": "🔕 조용히 보내기"
162165
"false": "🔔 알림과 함께 보내기"
166+
original_sender_profile:
167+
button:
168+
"true": "원본 작성자 프로필로 보내기"
169+
"false": "봇 프로필로 보내기"
170+
state:
171+
"true": "🪪 원본 작성자 프로필로 보내기"
172+
"false": "🤖 봇 프로필로 보내기"
163173
webhooks:
164174
name: "웹후크"
165175
description: "웹후크 활성화/비활성화하기"

src/settings.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,7 @@ def __init__(self, interaction: discore.Interaction, view: SettingsView, ctx: Da
11251125
super().__init__(interaction, view, ctx)
11261126
self.reply_to_message = bool(ctx.guild.reply_to_message)
11271127
self.reply_silently = bool(ctx.guild.reply_silently)
1128+
self.reply_as_original_sender_profile = bool(ctx.guild.reply_as_original_sender_profile)
11281129

11291130
@property
11301131
async def embed(self) -> discore.Embed:
@@ -1137,21 +1138,29 @@ async def embed(self) -> discore.Embed:
11371138
perms.append('send_messages_in_threads')
11381139
if self.reply_to_message:
11391140
perms.append('read_message_history')
1141+
if self.reply_as_original_sender_profile:
1142+
perms.append('manage_webhooks')
11401143
embed = discore.Embed(
11411144
title=f"{self.emoji} {t(self.name)}",
11421145
description=t(
11431146
'settings.reply_method.content',
11441147
state=t(f'settings.reply_method.reply.state.{l(self.reply_to_message)}', emoji=self.emoji),
11451148
silent=t(f'settings.reply_method.silent.state.{l(self.reply_silently)}'),
1149+
avatar=t(f'settings.reply_method.original_sender_profile.state.{l(self.reply_as_original_sender_profile)}'),
11461150
perms=format_perms(perms, self.ctx.channel.discord_object))
11471151
)
11481152
discore.set_embed_footer(self.bot, embed)
11491153
return embed
11501154

11511155
@property
11521156
async def option(self) -> discore.SelectOption:
1157+
has_missing_perms = (
1158+
(self.reply_to_message and is_missing_perm(['read_message_history'], self.ctx.channel.discord_object))
1159+
or (self.reply_as_original_sender_profile
1160+
and is_missing_perm(['manage_webhooks'], self.ctx.channel.discord_object))
1161+
)
11531162
return discore.SelectOption(
1154-
label=('⚠️ ' if self.reply_to_message and is_missing_perm(['read_message_history'], self.ctx.channel.discord_object) else '')
1163+
label=('⚠️ ' if has_missing_perms else '')
11551164
+ t(self.name),
11561165
value=self.id,
11571166
description=t(self.description),
@@ -1172,7 +1181,13 @@ async def items(self) -> List[discore.ui.Item]:
11721181
custom_id='reply_silently'
11731182
)
11741183
edit_callback(reply_silently_button, self.view, self.toggle_reply_silently)
1175-
return [reply_to_message_button, reply_silently_button]
1184+
original_sender_profile_button = discore.ui.Button(
1185+
style=discore.ButtonStyle.primary if self.reply_as_original_sender_profile else discore.ButtonStyle.secondary,
1186+
label=t(f'settings.reply_method.original_sender_profile.button.{l(self.reply_as_original_sender_profile)}'),
1187+
custom_id='reply_as_original_sender_profile'
1188+
)
1189+
edit_callback(original_sender_profile_button, self.view, self.toggle_reply_as_original_sender_profile)
1190+
return [reply_to_message_button, reply_silently_button, original_sender_profile_button]
11761191

11771192
async def toggle_reply_to_message(self, view: SettingsView, interaction: discore.Interaction, _) -> None:
11781193
self.reply_to_message = not self.reply_to_message
@@ -1184,6 +1199,11 @@ async def toggle_reply_silently(self, view: SettingsView, interaction: discore.I
11841199
self.ctx.guild.update({'reply_silently': self.reply_silently})
11851200
await view.refresh(interaction)
11861201

1202+
async def toggle_reply_as_original_sender_profile(self, view: SettingsView, interaction: discore.Interaction, _) -> None:
1203+
self.reply_as_original_sender_profile = not self.reply_as_original_sender_profile
1204+
self.ctx.guild.update({'reply_as_original_sender_profile': self.reply_as_original_sender_profile})
1205+
await view.refresh(interaction)
1206+
11871207

11881208
class WebhooksSetting(BaseSetting):
11891209
"""Represents the webhooks setting (respond to webhooks or not)"""

0 commit comments

Comments
 (0)