Skip to content
Merged
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
5 changes: 5 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def create_pem_from_base64(base64_string, filename):
SECRET_KEY = os.environ["SECRET_KEY"]
SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"]
MONGO_URL = os.environ["MONGO_URI"]
DISCORD_CLIENT_ID = os.environ["DISCORD_CLIENT_ID"]
DISCORD_CLIENT_SECRET = os.environ["DISCORD_CLIENT_SECRET"]
DISCORD_REDIRECT_URI = os.environ["DISCORD_REDIRECT_URI"]
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
DISCORD_GUILD_ID = os.environ["DISCORD_GUILD_ID"]

if IS_PROD:
RECAPTCHA_SITE_KEY = os.environ["RECAPTCHA_SITE_KEY"]
Expand Down
5 changes: 5 additions & 0 deletions dev_env.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ $env:MONGO_URI='mongodb://root:example@localhost:27017/'
$env:SENDGRID_API_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
$env:SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
$env:SERVER_PORT='8080'
$env:DISCORD_CLIENT_ID='12345'
$env:DISCORD_CLIENT_SECRET='secret'
$env:DISCORD_REDIRECT_URI='uri'
$env:DISCORD_BOT_TOKEN='bot_token'
$env:DISCORD_GUILD_ID='guild_id'
5 changes: 5 additions & 0 deletions dev_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export MONGO_URI=mongodb://root:example@localhost:27017/
export SENDGRID_API_KEY=192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf
export SECRET_KEY=192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf
export SERVER_PORT=8080
export DISCORD_CLIENT_ID=12345
export DISCORD_CLIENT_SECRET=secret
export DISCORD_REDIRECT_URI=uri
export DISCORD_BOT_TOKEN=bot_token
export DISCORD_GUILD_ID=guild_id
112 changes: 112 additions & 0 deletions discord_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import requests
import config
import discord_service

from requests.exceptions import HTTPError
from app_setup import app
from task_api import login_required
from flask import request, session, redirect, url_for, flash

DISCORD_API_BASE = 'https://discord.com/api'
CLIENT_ID = config.DISCORD_CLIENT_ID
CLIENT_SECRET = config.DISCORD_CLIENT_SECRET
REDIRECT_URI = config.DISCORD_REDIRECT_URI

@app.route("/api/v2/auth/discord/connect", methods=['GET'])
@login_required
def connect_discord():
discord_oauth_url = (
"https://discord.com/oauth2/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&scope=identify"
)
return redirect(discord_oauth_url)

@app.route("/api/v2/auth/discord/disconnect", methods=['GET'])
@login_required
def disconnect_discord():
username = session['username']
discord_service.unlink_discord_id(username)
discord_service.update_discord_name_sync(username, discord_name_sync_enabled=False)
flash('Discord authentication disconnected!')
return redirect(url_for('profile'))

@app.route('/api/v2/auth/discord/redirect', methods=['GET'])
@login_required
def authorize():
code = request.args.get('code')

try:
resp = exchange_code(code)
except HTTPError as e:
if (400 <= e.response.status_code < 500):
flash('Something went wrong, please try again...')
return redirect(url_for('profile'))

access_token = resp['access_token']

resp = retrieve_discord_id(access_token)

username = session['username']
discord_service.link_discord_id(username, resp['id'])

return redirect(url_for('profile'))

@app.route("/api/v2/discord/change-default-name/", methods=['POST'])
@login_required
def change_discord_default_name():
username = session['username']
discord_default_name = request.form['discord_username']

if (2 <= len(discord_default_name) <= 12):
discord_service.update_discord_default_name(username, discord_default_name=discord_default_name)
return {'success' : True}
else:
return {'success' : False, 'error' : "New name does not meet requirements."}

@app.route("/api/v2/discord/change-name-sync/", methods=['POST'])
@login_required
def change_discord_name_sync():
username = session['username']
data = request.form['discord_name_sync_enabled']
data = False if data == 'false' else True

discord_service.update_discord_name_sync(username, data)

return {'success' : True}

def exchange_code(code):
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept-Encoding': 'application/x-www-form-urlencoded'
}
data = {
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI,
'code': code
}
r = requests.post(
'%s/oauth2/token' % DISCORD_API_BASE,
data=data,
headers=headers,
auth=(CLIENT_ID, CLIENT_SECRET)
)

r.raise_for_status()

return r.json()

def retrieve_discord_id(access_token):
headers = {
'Authorization': 'Bearer ' + access_token
}
r = requests.get(
f'{DISCORD_API_BASE}/users/@me',
headers=headers,
)

r.raise_for_status()

return r.json()
163 changes: 163 additions & 0 deletions discord_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@

import config
import requests
import task_database
import tasklists

from app_setup import app
from dataclasses import dataclass, asdict
from task_types import TaskData

user_info_db = config.MONGO_CLIENT["TaskAppLoginDB"]
task_list_db = config.MONGO_CLIENT["TaskApp"]

DISCORD_API_BASE = 'https://discord.com/api/v10'
BOT_TOKEN = config.DISCORD_BOT_TOKEN
GUILD_ID = config.DISCORD_GUILD_ID

@dataclass
class DiscordAuthInfo:
discord_user_id: int
discord_username_default: str

def get_discord_auth_info(username: str) -> DiscordAuthInfo:
uidb = user_info_db['users']
users = list(uidb.find({'username': username}))
if len(users) == 0:
raise Exception("No user found with username " + username)
user_data = users[0]
return convert_database_auth_info(user_data)

def convert_database_auth_info(user_data: dict) -> DiscordAuthInfo:
if 'discord_auth_info' in user_data.keys():
if 'discord_user_id' in user_data['discord_auth_info'].keys():
return DiscordAuthInfo(
discord_user_id=user_data['discord_auth_info']['discord_user_id'],
discord_username_default=user_data['discord_auth_info']['discord_username_default'],
)

return get_new_discord_auth_info(user_data['username'])

def get_new_discord_auth_info(username: str) -> DiscordAuthInfo:
return DiscordAuthInfo(
discord_user_id='',
discord_username_default=username[:12],
)

def save_discord_auth_info(username: str, auth_info: DiscordAuthInfo):
uidb = user_info_db['users']
uidb.update_one({'username': username}, {'$set': {'discord_auth_info': asdict(auth_info)}})

def save_discord_link_status(username: str, discordLinked: bool):
tldb = task_list_db['taskLists']
tldb.update_one({'username': username}, {'$set': {'discordLinked': discordLinked}})

def save_discord_name_sync_enabled_status(username: str, discord_name_sync_enabled: bool):
tldb = task_list_db['taskLists']
tldb.update_one({'username': username}, {'$set': {'discordNameSyncEnabled': discord_name_sync_enabled}})
if (discord_name_sync_enabled):
name = generate_discord_nickname_from_user(username)
update_nickname_DISCORD(username, name)
else:
default_name = get_discord_auth_info(username).discord_username_default
update_nickname_DISCORD(username, default_name)


def link_discord_id(username: str, id: int):
auth_info: DiscordAuthInfo = get_discord_auth_info(username)
auth_info.discord_user_id = id
save_discord_auth_info(username, auth_info)
save_discord_link_status(username, True)

def unlink_discord_id(username: str):
auth_info: DiscordAuthInfo = get_discord_auth_info(username)
auth_info.discord_user_id = None
save_discord_auth_info(username, auth_info)
save_discord_link_status(username, False)

def update_discord_default_name(username: str, discord_default_name: str):
auth_info: DiscordAuthInfo = get_discord_auth_info(username)
auth_info.discord_username_default = discord_default_name
save_discord_auth_info(username, auth_info)

user = task_database.get_user(username)
if (user.discord_name_sync_enabled):
name = generate_discord_nickname_from_user(username)
update_nickname_DISCORD(username, name)
else:
update_nickname_DISCORD(username, discord_default_name)

def update_discord_name_sync(username: str, discord_name_sync_enabled: bool):
save_discord_name_sync_enabled_status(username, discord_name_sync_enabled)

def update_nickname_DISCORD(username: str, nickname: str) -> bool:
discord_user_id = get_discord_auth_info(username).discord_user_id

nickname = nickname[:32]

url = (
f"{DISCORD_API_BASE}/guilds/"
f"{GUILD_ID}/members/{discord_user_id}"
)
headers = {
"Authorization": f"Bot {BOT_TOKEN}",
"Content-Type": "application/json",
}

try:
response = requests.patch(
url,
headers=headers,
json={"nick": nickname},
timeout=10,
)

if response.status_code == 200:
print(f"Updated nickname for Discord user {discord_user_id}")
return True

if response.status_code == 404:
print(f"Discord user {discord_user_id} is not in the guild.")
save_discord_name_sync_enabled_status(username, discord_name_sync_enabled=False)
return False

if response.status_code == 403:
print("Bot lacks permission to update nicknames.")
return False

print(f"Discord API returned {response.status_code}: {response.text}")
return False

except requests.RequestException as e:
print(f"Failed to contact Discord: {e}")
return False

def get_discord_nickname(username: str, discord_username_default: str, tier: str, short_name: str) -> str:
short_tier = "" if len(tier) == 0 else tier[0].capitalize()
percentage = task_database.get_task_progress(username)[tier]['percent_complete']
return f"{discord_username_default} {percentage}%{short_tier} {short_name}"

def generate_discord_nickname_from_current_task(username: str, tier: str, task: TaskData) -> str:
discord_username = get_discord_auth_info(username).discord_username_default

return get_discord_nickname(username, discord_username, tier, task.short_name)

def generate_discord_nickname_from_completed_task(username: str, tier: str) -> str:
discord_username = get_discord_auth_info(username).discord_username_default

return get_discord_nickname(username, discord_username, tier, '')

def generate_discord_nickname_from_user(username: str) -> str:
user = task_database.get_user(username)
discord_username = get_discord_auth_info(username).discord_username_default

tier = user.current_tier()

if (tier is None):
return discord_username

current_task = user.current_task()
task_id = current_task[3] if current_task is not None else None
short_name = [n for n in tasklists.list_for_tier(tier) if n.id == task_id][0].short_name

return get_discord_nickname(username, discord_username, tier, short_name)
21 changes: 13 additions & 8 deletions static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,16 @@ body {
box-shadow: 0 0 5px var(--current-task-color);
}

.container {
.container-discord-settings, .container {
display: flex;
align-items: center;
justify-content: center;
}

.container-discord-settings {
padding-top: 3rem;
padding-bottom: 3rem;
}

.faq-container {
display: flex;
Expand Down Expand Up @@ -1445,7 +1450,7 @@ nav svg {
outline: 2px solid var(--current-task-color);
}

.profile-content {
.discord-options, .profile-content {
display: flex;
flex-direction: column;
align-items: center;
Expand All @@ -1454,7 +1459,7 @@ nav svg {
row-gap: 20px;
}

.profile-content input {
.discord-options input, .profile-content input {
background-color: var(--main-bg-color);
color: var(--current-task-color);
border: 2px solid var(--main-accent-color);
Expand All @@ -1466,17 +1471,17 @@ nav svg {
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.profile-content input:hover {
.discord-options input:hover, .profile-content input:hover {
border-color: var(--current-task-color);
cursor: pointer;
}

.profile-content input:focus {
.discord-options input:focus, .profile-content input:focus {
border-color: var(--current-task-color);
box-shadow: 0 0 5px var(--current-task-color);
}

.email-details {
.discord-name-details, .email-details {
display: flex;
}

Expand Down Expand Up @@ -1550,7 +1555,7 @@ input:checked + .slider:before {

}

.email-input-box form {
.discord-name-input-box form, .email-input-box form {
display: flex;
flex-direction: column;
gap: 5px;
Expand All @@ -1562,7 +1567,7 @@ input:checked + .slider:before {
gap: 5px;
}

.password-details form {
.discord-name-details form, .password-details form {
display: flex;
flex-direction: column;
gap: 5px;
Expand Down
Loading