Skip to content

Commit 554438b

Browse files
committed
Implement /link & /unlink commands
1 parent 8d279b5 commit 554438b

File tree

4 files changed

+182
-8
lines changed

4 files changed

+182
-8
lines changed

app/bot.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ async def on_ready(self):
1111
app.session.logger.info(f'Logged in as {self.user}.')
1212
app.session.filters.populate()
1313
await self.load_cogs()
14-
14+
1515
async def load_cogs(self):
1616
await self.load_extension("app.commands.kms")
17+
await self.load_extension("app.commands.link")
1718
await self.load_extension("app.commands.bancho")
19+
await self.tree.sync()
1820

1921
def run():
2022
intents = discord.Intents.default()

app/cog.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class BaseCog(Cog):
1313
def __init__(self) -> None:
1414
self.bot = session.bot
15+
self.guild = session.bot.guilds[0]
1516
self.redis = session.redis_async
1617
self.logger = session.logger
1718
self.events = session.events
@@ -25,4 +26,25 @@ async def run_async(func: Callable, *args):
2526
return await asyncio.get_event_loop().run_in_executor(None, func, *args)
2627

2728
async def resolve_user(self, discord_id: int) -> DBUser | None:
28-
return await self.run_async(users.fetch_by_discord_id, discord_id)
29+
return await self.run_async(
30+
users.fetch_by_discord_id,
31+
discord_id
32+
)
33+
34+
async def resolve_user_by_name(self, username: str) -> DBUser | None:
35+
return await self.run_async(
36+
users.fetch_by_name_extended,
37+
username
38+
)
39+
40+
async def update_user(self, user_id: int, updates: dict) -> int:
41+
return await self.run_async(
42+
users.update,
43+
user_id, updates
44+
)
45+
46+
async def submit_event(self, name: str, *args) -> None:
47+
return await self.run_async(
48+
self.events.submit,
49+
name, *args
50+
)

app/commands/bancho.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,6 @@ async def silence_user(self, user: DBUser, duration: float, reason: str) -> None
129129
user, duration, reason
130130
)
131131

132-
async def submit_event(self, name: str, *args) -> None:
133-
return await self.run_async(
134-
self.events.submit,
135-
name, *args
136-
)
137-
138132
async def create_message(self, username: str, target: str, content: str) -> None:
139133
return await self.run_async(
140134
messages.create,

app/commands/link.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
2+
from discord.ext.commands import Cog, Bot, command
3+
from discord.ui import Modal, TextInput
4+
from discord import app_commands
5+
6+
from app.common.database.objects import DBUser
7+
from app.common.cache import status
8+
from app.cog import BaseCog
9+
10+
import discord
11+
import random
12+
import string
13+
import time
14+
15+
class AccountLinking(BaseCog):
16+
def __init__(self) -> None:
17+
super().__init__()
18+
self.member_role = discord.utils.get(self.guild.roles, name="Member")
19+
20+
@app_commands.command(name="link", description="Link your account to Titanic!")
21+
@app_commands.describe(username="Your Titanic! username")
22+
async def link_account(self, interaction: discord.Interaction, username: str) -> None:
23+
if existing_user := await self.resolve_user(interaction.user.id):
24+
return await interaction.response.send_message(
25+
"Your account is already linked to Titanic! "
26+
"Use /unlink to unlink your current account.",
27+
ephemeral=True
28+
)
29+
30+
if not (target_user := await self.resolve_user_by_name(username)):
31+
return await interaction.response.send_message(
32+
"No user found with that name.",
33+
ephemeral=True
34+
)
35+
36+
if target_user.discord_id:
37+
return await interaction.response.send_message(
38+
"This user is already linked to another Discord account.",
39+
ephemeral=True
40+
)
41+
42+
if not status.exists(target_user.id):
43+
return await interaction.response.send_message(
44+
"Please log into the game and try again!",
45+
ephemeral=True
46+
)
47+
48+
self.logger.info(f'[{interaction.user}] -> Starting linking process...')
49+
50+
# Generate random 6-letter code which will be sent over DMs
51+
code = ''.join(random.choices(string.ascii_lowercase, k=6))
52+
await self.submit_event('link', target_user.id, code)
53+
54+
embed = discord.Embed(
55+
title="🔗 Link Your Account",
56+
description=(
57+
f"You are linking the account: **{target_user.name}**\n"
58+
"Click the button below to enter your code."
59+
),
60+
color=discord.Color.blurple()
61+
)
62+
embed.set_footer(text="This message is only visible to you.")
63+
64+
await interaction.response.send_message(
65+
view=LinkingView(code, target_user, self),
66+
embed=embed,
67+
ephemeral=True
68+
)
69+
70+
@app_commands.command(name="unlink", description="Unlink your account from Titanic!")
71+
async def unlink_account(self, interaction: discord.Interaction) -> None:
72+
if not (linked_user := await self.resolve_user(interaction.user.id)):
73+
return await interaction.response.send_message(
74+
"Your account is not linked to Titanic!",
75+
ephemeral=True
76+
)
77+
78+
await self.update_user(
79+
linked_user.id,
80+
{"discord_id": None}
81+
)
82+
83+
await interaction.response.send_message(
84+
"You have successfully unlinked your account from Titanic!",
85+
ephemeral=True
86+
)
87+
88+
@Cog.listener()
89+
async def on_member_join(self, member: discord.Member) -> None:
90+
# Try to check if user already has their account linked to Titanic!
91+
linked_user = await self.resolve_user(member.id)
92+
93+
if not linked_user:
94+
return
95+
96+
# Re-add member role
97+
await member.add_roles(self.member_role)
98+
99+
class LinkingView(discord.ui.View):
100+
def __init__(self, code: str, target_user: DBUser, cog: "AccountLinking"):
101+
super().__init__(timeout=60*5)
102+
self.target_user = target_user
103+
self.code = code
104+
self.cog = cog
105+
106+
@discord.ui.button(label="Enter Code", style=discord.ButtonStyle.primary)
107+
async def enter_code(
108+
self,
109+
interaction: discord.Interaction,
110+
button: discord.ui.Button
111+
) -> None:
112+
modal = AccountLinkingModal(self.code, self.target_user, self.cog)
113+
await interaction.response.send_modal(modal)
114+
115+
class AccountLinkingModal(Modal):
116+
def __init__(self, valid_code: str, target_user: DBUser, cog: "AccountLinking") -> None:
117+
super().__init__(
118+
title="Link your account",
119+
timeout=60*5
120+
)
121+
self.code = TextInput(
122+
label="Enter in-game code",
123+
placeholder="abc123",
124+
required=True,
125+
max_length=6
126+
)
127+
self.add_item(self.code)
128+
self.target_user = target_user
129+
self.valid_code = valid_code
130+
self.cog = cog
131+
132+
async def on_submit(self, interaction: discord.Interaction) -> None:
133+
self.cog.logger.info(
134+
f'[{interaction.user}] -> Entered code: "{self.code.value}"'
135+
)
136+
137+
if self.code.value != self.valid_code:
138+
return await interaction.response.send_message(
139+
"Invalid code. Please try again.",
140+
ephemeral=True
141+
)
142+
143+
await self.cog.update_user(
144+
self.target_user.id,
145+
{"discord_id": interaction.user.id}
146+
)
147+
await interaction.response.send_message(
148+
"Account linked successfully!",
149+
ephemeral=True
150+
)
151+
self.cog.logger.info(
152+
f'[{interaction.user}] -> Linked account: {self.target_user.name}'
153+
)
154+
155+
async def setup(bot: Bot):
156+
await bot.add_cog(AccountLinking())

0 commit comments

Comments
 (0)