Skip to content

Commit

Permalink
Merge pull request #37 from redstone-squid/verify
Browse files Browse the repository at this point in the history
Implement linking of minecraft account to discord account
  • Loading branch information
Glinte authored Aug 4, 2024
2 parents fa1e80e + 1f9994d commit 46504bd
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 11 deletions.
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ BOT_TOKEN=your_token_here
# Supabase
SUPABASE_URL=your_url_here
SUPABASE_KEY=your_key_here
CATBOX_USERHASH=your_hash_here

# Catbox
CATBOX_USERHASH=your_hash_here

# Provide a secret to trusted minecraft servers to autheticate users
SYNERGY_SECRET=your_secret_here
30 changes: 30 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Simple FastAPI server to generate verification codes for users."""
import os
import random

from fastapi import FastAPI, HTTPException

from database.database import DatabaseManager

app = FastAPI()


@app.get("/verify")
async def get_verification_code(uuid: str, super_duper_secret: str) -> int:
"""Generate a verification code for a user."""
if super_duper_secret != os.environ["SYNERGY_SECRET"]:
raise HTTPException(status_code=401, detail="Unauthorized")

code = random.randint(100000, 999999)
await DatabaseManager().table("verification_codes").insert({"minecraft_uuid": uuid, "code": code}).execute()
return code


if __name__ == "__main__":
import asyncio
import uvicorn
from dotenv import load_dotenv
load_dotenv()

asyncio.run(DatabaseManager.setup())
uvicorn.run(app, host="0.0.0.0", port=3000)
2 changes: 2 additions & 0 deletions bot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dotenv import load_dotenv

from bot.submission.submit import SubmissionsCog
from bot.verify import VerifyCog
from database.database import DatabaseManager
from database.utils import utcnow
from bot.config import OWNER_SERVER_ID, OWNER_ID, BOT_NAME, BOT_VERSION, PREFIX, DEV_MODE, DEV_PREFIX
Expand Down Expand Up @@ -126,6 +127,7 @@ async def setup_hook(self) -> None:
await self.add_cog(Listeners(self))
await self.add_cog(HelpCog(self))
await self.load_extension("jishaku")
await self.add_cog(VerifyCog(self))


async def main():
Expand Down
34 changes: 33 additions & 1 deletion bot/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import re
from traceback import format_tb
from types import TracebackType
from typing import overload, Literal
from typing import overload, Literal, Any

import discord
from discord import Message, Webhook
from discord.abc import Messageable

from bot.config import OWNER_ID, PRINT_TRACEBACKS
from database.database import DatabaseManager
from database.schema import RECORD_CATEGORIES, DOOR_ORIENTATION_NAMES

discord_red = 0xF04747
discord_yellow = 0xFAA61A
Expand Down Expand Up @@ -152,3 +154,33 @@ async def __aexit__(
if self.delete_on_exit:
await self.sent_message.delete()
return False


async def parse_build_title(title: str) -> dict[str, Any]:
"""Parses a title into a category and a name.
A build title should be in the format of:
```
[Record Category] [component restrictions]+ <door size> [wiring placement restrictions]+ <door type> <orientation>
```
Args:
title: The title to parse
Returns:
A dictionary containing the parsed information.
"""
data = {}
words = title.split()
if words[0].title() in RECORD_CATEGORIES:
data["record_category"] = words.pop(0)

if words[-1].title() not in DOOR_ORIENTATION_NAMES:
raise ValueError(f"Invalid orientation. Expected one of {DOOR_ORIENTATION_NAMES}, found {words[-1]}")
else:
data["category"] = "Door"

db = DatabaseManager()
# Parse component restrictions
component_restrictions = await db.table("restrictions").select("name").eq("build_category", data["category"]).eq("type", "component").execute()

43 changes: 43 additions & 0 deletions bot/verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""A cog for verifying minecraft accounts."""

from __future__ import annotations

from typing import TYPE_CHECKING

from discord.ext.commands import Cog, hybrid_command, Context

from bot.submission.ui import ConfirmationView
from database.user import link_minecraft_account, unlink_minecraft_account

if TYPE_CHECKING:
from bot.main import RedstoneSquid

class VerifyCog(Cog, name="verify"):
def __init__(self, bot: RedstoneSquid):
self.bot = bot

@hybrid_command()
async def link(self, ctx: Context, code: str):
"""Link your minecraft account."""
if await link_minecraft_account(ctx.author.id, code):
await ctx.send("Your discord account has been linked with your minecraft account.")
else:
await ctx.send("Invalid code. Please generate a new code and try again.")

@hybrid_command()
async def unlink(self, ctx: Context):
"""Unlink your minecraft account."""
view = ConfirmationView()
await ctx.send("Are you sure you want to unlink your minecraft account?", view=view)

await view.wait()
if view.value:
if await unlink_minecraft_account(ctx.author.id):
await ctx.send("Your discord account has been unlinked from your minecraft account.")
else:
await ctx.send("An error occurred while unlinking your account. Please try again later.")


async def setup(bot: RedstoneSquid):
"""Called by discord.py when the cog is added to the bot via bot.load_extension."""
await bot.add_cog(VerifyCog(bot))
21 changes: 15 additions & 6 deletions database/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
)
from database.database import DatabaseManager
from database.server_settings import get_server_setting
from database.user import add_user
from database.utils import utcnow
from database.enums import Status, Category
from bot import utils
from bot.config import VERSIONS_LIST


all_build_columns = "*, versions(*), build_links(*), build_creators(*), types(*), restrictions(*), doors(*), extenders(*), utilities(*), entrances(*)"
all_build_columns = "*, versions(*), build_links(*), build_creators(*), users(*), types(*), restrictions(*), doors(*), extenders(*), utilities(*), entrances(*)"
"""All columns that needs to be joined in the build table to get all the information about a build."""


Expand Down Expand Up @@ -228,8 +229,8 @@ def from_json(data: dict[str, Any]) -> Build:

build.information = data["information"]

creators: list[dict[str, Any]] = data.get("build_creators", [])
build.creators_ign = [creator["creator_ign"] for creator in creators]
creators: list[dict[str, Any]] = data.get("users", [])
build.creators_ign = [creator["ign"] for creator in creators]

versions: list[dict[str, Any]] = data.get("versions", [])
build.functional_versions = [version["full_name_temp"] for version in versions]
Expand Down Expand Up @@ -462,9 +463,17 @@ async def _update_build_links_table(self, data: dict[str, Any]) -> None:

async def _update_build_creators_table(self, data: dict[str, Any]) -> None:
"""Updates the build_creators table with the given data."""
build_creators_data = list(
{"build_id": self.id, "creator_ign": creator} for creator in data.get("creators_ign", [])
)
db = DatabaseManager()
creator_ids = []
for creator_ign in data.get("creators_ign", []):
response = await db.table("users").select("id").eq("ign", creator_ign).maybe_single().execute()
if response:
creator_ids.append(response.data["id"])
else:
creator_id = add_user(ign=creator_ign)
creator_ids.append(creator_id)

build_creators_data = [{"build_id": self.id, "user_id": user_id} for user_id in creator_ids]
if build_creators_data:
await DatabaseManager().table("build_creators").upsert(build_creators_data).execute()

Expand Down
22 changes: 22 additions & 0 deletions database/migrations/20240804093229_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
discord_id BIGINT,
minecraft_uuid UUID,
ign TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

INSERT INTO users(ign)
select build_creators.creator_ign
from build_creators
where build_creators.creator_ign not in (select ign from users);

ALTER TABLE build_creators ADD COLUMN user_id INT REFERENCES users(id);

UPDATE build_creators
SET user_id = users.id
FROM users
WHERE build_creators.creator_ign = users.ign;

ALTER TABLE build_creators DROP COLUMN creator_ign;
ALTER TABLE build_creators ADD PRIMARY KEY (build_id, user_id);
9 changes: 9 additions & 0 deletions database/migrations/20240804101429_verification_code.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE verification_codes (
id SMALLSERIAL PRIMARY KEY,
minecraft_uuid UUID NOT NULL,
code TEXT NOT NULL,
created TIMESTAMP DEFAULT now() NOT NULL,
expires TIMESTAMP DEFAULT now() + INTERVAL '10 minutes' NOT NULL
);

ALTER TABLE users ALTER COLUMN created_at SET DATA TYPE timestamp;
75 changes: 75 additions & 0 deletions database/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Handles user data and operations."""
import requests

from utils import utcnow
from database import DatabaseManager


async def add_user(user_id: int = None, ign: str = None) -> int:
"""Add a user to the database.
Args:
user_id: The user's Discord ID.
ign: The user's in-game name.
Returns:
The ID of the new user.
"""
if user_id is None and ign is None:
raise ValueError("No user data provided.")

db = DatabaseManager()
response = await db.table("users").insert({"discord_id": user_id, "ign": ign}).execute()
return response.data[0]["id"]


async def link_minecraft_account(user_id: int, code: str) -> bool:
"""Using a verification code, link a user's Discord account with their Minecraft account.
Args:
user_id: The user's Discord ID.
code: The verification code.
Returns:
True if the code is valid and the accounts are linked, False otherwise.
"""
db = DatabaseManager()

response = await db.table("verification_codes").select("minecraft_uuid").eq("code", code).gt("expires", utcnow()).execute()
if not response.data:
return False
minecraft_uuid = response.data[0]["minecraft_uuid"]

# TODO: This currently does not check if the ign is already in use without a UUID or discord ID given.
response = await db.table("users").update({"minecraft_uuid": minecraft_uuid, "ign": get_minecraft_username(minecraft_uuid)}).eq("discord_id", user_id).execute()
if not response.data:
await db.table("users").insert({"discord_id": user_id, "minecraft_uuid": minecraft_uuid, "ign": get_minecraft_username(minecraft_uuid)}).execute()
return True


async def unlink_minecraft_account(user_id: int) -> bool:
"""Unlink a user's Minecraft account from their Discord account.
Args:
user_id: The user's Discord ID.
Returns:
True if the accounts were successfully unlinked, False otherwise.
"""
db = DatabaseManager()
await db.table("users").update({"minecraft_uuid": None}).eq("discord_id", user_id).execute()
return True


def get_minecraft_username(user_uuid: str) -> str:
"""Get a user's Minecraft username from their UUID.
Args:
user_uuid: The user's Minecraft UUID.
Returns:
The user's Minecraft username.
"""
# https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape
response = requests.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{user_uuid}")
return response.json()["name"]
2 changes: 2 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ supabase-py-async
python-dotenv
jishaku
requests-toolbelt
fastapi
uvicorn
20 changes: 17 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ aiosignal==1.3.1
annotated-types==0.7.0
# via pydantic
anyio==4.4.0
# via httpx
# via
# httpx
# starlette
argcomplete==3.3.0
# via commitizen
astunparse==1.6.3
Expand All @@ -34,7 +36,9 @@ charset-normalizer==3.3.2
# commitizen
# requests
click==8.1.7
# via jishaku
# via
# jishaku
# uvicorn
colorama==0.4.6
# via
# click
Expand All @@ -51,6 +55,8 @@ discord==2.3.2
# via -r requirements.in
discord-py==2.4.0
# via discord
fastapi==0.112.0
# via -r requirements.in
frozenlist==1.4.1
# via
# aiohttp
Expand All @@ -66,7 +72,9 @@ gotrue==2.5.5
gspread==6.1.2
# via -r requirements.in
h11==0.14.0
# via httpcore
# via
# httpcore
# uvicorn
h2==4.1.0
# via httpx
hpack==4.0.0
Expand Down Expand Up @@ -126,6 +134,7 @@ pyasn1-modules==0.4.0
# oauth2client
pydantic==2.8.2
# via
# fastapi
# gotrue
# postgrest
pydantic-core==2.20.1
Expand Down Expand Up @@ -165,6 +174,8 @@ sniffio==1.3.1
# via
# anyio
# httpx
starlette==0.37.2
# via fastapi
storage3==0.7.7
# via supabase-py-async
strenum==0.4.15
Expand All @@ -179,12 +190,15 @@ tomlkit==0.13.0
# via commitizen
typing-extensions==4.12.2
# via
# fastapi
# pydantic
# pydantic-core
# realtime
# storage3
urllib3==2.2.2
# via requests
uvicorn==0.30.5
# via -r requirements.in
wcwidth==0.2.13
# via prompt-toolkit
websockets==12.0
Expand Down

0 comments on commit 46504bd

Please sign in to comment.