Compare commits
	
		
			3 Commits 
		
	
	
		
			c05794ad99
			...
			00e1cedfcd
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						00e1cedfcd | |
| 
							
							
								
									
								
								 | 
						ff6bc7f512 | |
| 
							
							
								
									
								
								 | 
						2d3f7f68fe | 
| 
						 | 
					@ -4,6 +4,7 @@
 | 
				
			||||||
/* eslint-disable */
 | 
					/* eslint-disable */
 | 
				
			||||||
import type { PlayerSchema } from './PlayerSchema';
 | 
					import type { PlayerSchema } from './PlayerSchema';
 | 
				
			||||||
export type GetUserResponse = {
 | 
					export type GetUserResponse = {
 | 
				
			||||||
 | 
					    discordId?: (string | null);
 | 
				
			||||||
    isAdmin?: boolean;
 | 
					    isAdmin?: boolean;
 | 
				
			||||||
    realUser: (PlayerSchema | null);
 | 
					    realUser: (PlayerSchema | null);
 | 
				
			||||||
    steamId: string;
 | 
					    steamId: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
/* tslint:disable */
 | 
					/* tslint:disable */
 | 
				
			||||||
/* eslint-disable */
 | 
					/* eslint-disable */
 | 
				
			||||||
export type PlayerSchema = {
 | 
					export type PlayerSchema = {
 | 
				
			||||||
 | 
					    discordId?: (string | null);
 | 
				
			||||||
    isAdmin?: boolean;
 | 
					    isAdmin?: boolean;
 | 
				
			||||||
    steamId: string;
 | 
					    steamId: string;
 | 
				
			||||||
    username: string;
 | 
					    username: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import type { RoleSchema } from './RoleSchema';
 | 
				
			||||||
export type ViewTeamMembersResponse = {
 | 
					export type ViewTeamMembersResponse = {
 | 
				
			||||||
    availability: Array<number>;
 | 
					    availability: Array<number>;
 | 
				
			||||||
    createdAt: string;
 | 
					    createdAt: string;
 | 
				
			||||||
 | 
					    discordId?: (string | null);
 | 
				
			||||||
    isAdmin?: boolean;
 | 
					    isAdmin?: boolean;
 | 
				
			||||||
    isTeamLeader?: boolean;
 | 
					    isTeamLeader?: boolean;
 | 
				
			||||||
    playtime: number;
 | 
					    playtime: number;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ export const useAuthStore = defineStore("auth", () => {
 | 
				
			||||||
  const hasCheckedAuth = ref(false);
 | 
					  const hasCheckedAuth = ref(false);
 | 
				
			||||||
  const isAdmin = ref(false);
 | 
					  const isAdmin = ref(false);
 | 
				
			||||||
  const realUser = ref<PlayerSchema | null>(null);
 | 
					  const realUser = ref<PlayerSchema | null>(null);
 | 
				
			||||||
 | 
					  const discordId = ref<string | null>("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const router = useRouter();
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,6 +40,7 @@ export const useAuthStore = defineStore("auth", () => {
 | 
				
			||||||
        user.value = response;
 | 
					        user.value = response;
 | 
				
			||||||
        isAdmin.value = response.isAdmin || (response.realUser?.isAdmin ?? false);
 | 
					        isAdmin.value = response.isAdmin || (response.realUser?.isAdmin ?? false);
 | 
				
			||||||
        realUser.value = response.realUser ?? null;
 | 
					        realUser.value = response.realUser ?? null;
 | 
				
			||||||
 | 
					        discordId.value = response.discordId ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return response;
 | 
					        return response;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -116,6 +118,7 @@ export const useAuthStore = defineStore("auth", () => {
 | 
				
			||||||
    username,
 | 
					    username,
 | 
				
			||||||
    isAdmin,
 | 
					    isAdmin,
 | 
				
			||||||
    realUser,
 | 
					    realUser,
 | 
				
			||||||
 | 
					    discordId,
 | 
				
			||||||
    isLoggedIn,
 | 
					    isLoggedIn,
 | 
				
			||||||
    hasCheckedAuth,
 | 
					    hasCheckedAuth,
 | 
				
			||||||
    isRegistering,
 | 
					    isRegistering,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,20 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { useAuthStore } from "@/stores/auth";
 | 
					import { useAuthStore } from "@/stores/auth";
 | 
				
			||||||
import { onMounted, ref } from "vue";
 | 
					import { computed, onMounted, ref } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//const baseUrl = window.location.origin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const redirectUri = computed(() => {
 | 
				
			||||||
 | 
					  return encodeURIComponent(window.location.origin + "/settings");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const oauthUrl = computed(() => {
 | 
				
			||||||
 | 
					  return "https://discord.com/oauth2/authorize" +
 | 
				
			||||||
 | 
					    "?client_id=1372254613692219392" +
 | 
				
			||||||
 | 
					    "&response_type=code" +
 | 
				
			||||||
 | 
					    `&redirect_uri=${redirectUri.value}` +
 | 
				
			||||||
 | 
					    "&scope=identify";
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const displayName = ref("");
 | 
					const displayName = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +39,29 @@ onMounted(() => {
 | 
				
			||||||
        </h3>
 | 
					        </h3>
 | 
				
			||||||
        <input v-model="displayName" />
 | 
					        <input v-model="displayName" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="form-group margin">
 | 
				
			||||||
 | 
					        <h3>
 | 
				
			||||||
 | 
					          Discord Account
 | 
				
			||||||
 | 
					        </h3>
 | 
				
			||||||
 | 
					        <!--p>
 | 
				
			||||||
 | 
					          Link your Discord account to your profile to enable Discord
 | 
				
			||||||
 | 
					          integration features. Contact
 | 
				
			||||||
 | 
					          <a href="https://discord.com/users/195789918474207233">@pyrofromcsgo</a>
 | 
				
			||||||
 | 
					          if you would like to manually link your account without logging in
 | 
				
			||||||
 | 
					          through Discord.
 | 
				
			||||||
 | 
					        </p-->
 | 
				
			||||||
 | 
					        <p v-if="authStore.discordId">
 | 
				
			||||||
 | 
					          Linked to Discord account <code>{{ authStore.discordId }}</code>.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        <p v-else>
 | 
				
			||||||
 | 
					          Discord OAuth is not yet implemented.
 | 
				
			||||||
 | 
					          Contact <a href="https://discord.com/users/195789918474207233">@pyrofromcsgo</a>
 | 
				
			||||||
 | 
					          if you would like to link your Discord account.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        <!--a :href="oauthUrl">
 | 
				
			||||||
 | 
					          <button>Link Discord Account</button>
 | 
				
			||||||
 | 
					        </a-->
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
      <div class="form-group margin">
 | 
					      <div class="form-group margin">
 | 
				
			||||||
        <div class="action-buttons">
 | 
					        <div class="action-buttons">
 | 
				
			||||||
          <button class="accent" @click="save">Save</button>
 | 
					          <button class="accent" @click="save">Save</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
# Use an official Python runtime as a parent image
 | 
					# Use an official Python runtime as a parent image
 | 
				
			||||||
FROM python:3.12-slim
 | 
					FROM python:3.13-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY requirements.txt /
 | 
					COPY requirements.txt /
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,8 @@ from flask import Flask
 | 
				
			||||||
from flask_migrate import Migrate
 | 
					from flask_migrate import Migrate
 | 
				
			||||||
from flask_sqlalchemy import SQLAlchemy
 | 
					from flask_sqlalchemy import SQLAlchemy
 | 
				
			||||||
from sqlalchemy import MetaData
 | 
					from sqlalchemy import MetaData
 | 
				
			||||||
from sqlalchemy.orm import DeclarativeBase
 | 
					from sqlalchemy.engine import create_engine
 | 
				
			||||||
 | 
					from sqlalchemy.orm import DeclarativeBase, scoped_session, sessionmaker
 | 
				
			||||||
from celery import Celery, Task
 | 
					from celery import Celery, Task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BaseModel(DeclarativeBase):
 | 
					class BaseModel(DeclarativeBase):
 | 
				
			||||||
| 
						 | 
					@ -74,7 +75,24 @@ metadata = MetaData(naming_convention=convention)
 | 
				
			||||||
def create_db() -> SQLAlchemy:
 | 
					def create_db() -> SQLAlchemy:
 | 
				
			||||||
    return SQLAlchemy(model_class=BaseModel, metadata=metadata)
 | 
					    return SQLAlchemy(model_class=BaseModel, metadata=metadata)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_isolated_db_session(database_uri: str | None):
 | 
				
			||||||
 | 
					    database_uri = environ.get("DATABASE_URI") or DATABASE_URI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not database_uri:
 | 
				
			||||||
 | 
					        raise ValueError("Database URI is not provided")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    engine = create_engine(database_uri)
 | 
				
			||||||
 | 
					    isolated_db = scoped_session(
 | 
				
			||||||
 | 
					        sessionmaker(
 | 
				
			||||||
 | 
					            autocommit=False,
 | 
				
			||||||
 | 
					            autoflush=False,
 | 
				
			||||||
 | 
					            bind=engine,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    return isolated_db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app = create_app()
 | 
					app = create_app()
 | 
				
			||||||
#db = SQLAlchemy(model_class=BaseModel, metadata=metadata)
 | 
					#db = SQLAlchemy(model_class=BaseModel, metadata=metadata)
 | 
				
			||||||
db = create_db()
 | 
					db = create_db()
 | 
				
			||||||
 | 
					db_session: scoped_session = db.session
 | 
				
			||||||
migrate = Migrate(app, db, render_as_batch=True)
 | 
					migrate = Migrate(app, db, render_as_batch=True)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,192 @@
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import discord
 | 
				
			||||||
 | 
					from discord.ext import commands
 | 
				
			||||||
 | 
					import app_db
 | 
				
			||||||
 | 
					import models
 | 
				
			||||||
 | 
					from models.event import Event
 | 
				
			||||||
 | 
					from models.player import Player
 | 
				
			||||||
 | 
					from models.player_event import PlayerEvent
 | 
				
			||||||
 | 
					from models.player_team import PlayerTeam
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app_db.db_session = app_db.create_isolated_db_session(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					guild_id_str = os.getenv("GUILD_ID")
 | 
				
			||||||
 | 
					guild_id = discord.Object(id=int(guild_id_str)) if guild_id_str else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					discord_token = os.getenv("DISCORD_TOKEN")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if not discord_token:
 | 
				
			||||||
 | 
					    raise ValueError("DISCORD_TOKEN environment variable not set.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventModal(discord.ui.Modal):
 | 
				
			||||||
 | 
					    event_name = discord.ui.TextInput(
 | 
				
			||||||
 | 
					        label="Event Name",
 | 
				
			||||||
 | 
					        placeholder="Enter the event name",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event_description = discord.ui.TextInput(
 | 
				
			||||||
 | 
					        label="Event Description",
 | 
				
			||||||
 | 
					        placeholder="Describe the event",
 | 
				
			||||||
 | 
					        style=discord.TextStyle.long,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, event: Event):
 | 
				
			||||||
 | 
					        self.event = event
 | 
				
			||||||
 | 
					        self.event_name.default = event.name
 | 
				
			||||||
 | 
					        self.event_description.default = event.description
 | 
				
			||||||
 | 
					        super().__init__(title="Event Details")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def on_submit(self, interaction: discord.Interaction):
 | 
				
			||||||
 | 
					        player_team = app_db.db_session.query(
 | 
				
			||||||
 | 
					            PlayerTeam
 | 
				
			||||||
 | 
					        ).where(
 | 
				
			||||||
 | 
					            PlayerTeam.team_id == self.event.team_id,
 | 
				
			||||||
 | 
					        ).join(
 | 
				
			||||||
 | 
					            Player,
 | 
				
			||||||
 | 
					            Player.steam_id == PlayerTeam.player_id
 | 
				
			||||||
 | 
					        ).where(
 | 
				
			||||||
 | 
					            Player.discord_id == interaction.user.id
 | 
				
			||||||
 | 
					        ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not player_team or not player_team.is_team_leader:
 | 
				
			||||||
 | 
					            await interaction.response.send_message(
 | 
				
			||||||
 | 
					                "You are not authorized to edit this event.",
 | 
				
			||||||
 | 
					                ephemeral=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.event.name = self.event_name.value
 | 
				
			||||||
 | 
					        self.event.description = self.event_description.value
 | 
				
			||||||
 | 
					        app_db.db_session.commit()
 | 
				
			||||||
 | 
					        self.event.update_discord_message()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await interaction.response.send_message("Event details updated.", ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async def handle_update_attendance(
 | 
				
			||||||
 | 
					    player: Player,
 | 
				
			||||||
 | 
					    event: Event,
 | 
				
			||||||
 | 
					    interaction: discord.Interaction,
 | 
				
			||||||
 | 
					    custom_id: str
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    player_event = app_db.db_session.query(
 | 
				
			||||||
 | 
					        PlayerEvent
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerEvent.player_id == player.steam_id,
 | 
				
			||||||
 | 
					        PlayerEvent.event_id == event.id
 | 
				
			||||||
 | 
					    ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if custom_id == "click_not_attending":
 | 
				
			||||||
 | 
					        if player_event:
 | 
				
			||||||
 | 
					            app_db.db_session.delete(player_event)
 | 
				
			||||||
 | 
					            app_db.db_session.commit()
 | 
				
			||||||
 | 
					            event.update_discord_message()
 | 
				
			||||||
 | 
					            await interaction.response.defer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not player_event:
 | 
				
			||||||
 | 
					        player_event = PlayerEvent()
 | 
				
			||||||
 | 
					        player_event.event_id = event.id
 | 
				
			||||||
 | 
					        player_event.player_id = player.steam_id
 | 
				
			||||||
 | 
					        app_db.db_session.add(player_event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    player_event.has_confirmed = custom_id == "click_attending"
 | 
				
			||||||
 | 
					    app_db.db_session.commit()
 | 
				
			||||||
 | 
					    event.update_discord_message()
 | 
				
			||||||
 | 
					    await interaction.response.defer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Client(commands.Bot):
 | 
				
			||||||
 | 
					    async def on_ready(self):
 | 
				
			||||||
 | 
					        if guild_id:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                synced = await self.tree.sync(guild=guild_id)
 | 
				
			||||||
 | 
					                print(f"Ready! Synced {len(synced)} commands.")
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                print(f"Failed to sync commands: {e}")
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def on_interaction(self, interaction: discord.Interaction):
 | 
				
			||||||
 | 
					        if interaction.response.is_done():
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if interaction.type == discord.InteractionType.component and interaction.message:
 | 
				
			||||||
 | 
					            # Handle button interactions here
 | 
				
			||||||
 | 
					            if interaction.data is None or not "custom_id" in interaction.data:
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            interactions = [
 | 
				
			||||||
 | 
					                "click_attending",
 | 
				
			||||||
 | 
					                "click_pending",
 | 
				
			||||||
 | 
					                "click_not_attending",
 | 
				
			||||||
 | 
					                "click_edit_event",
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if interaction.data["custom_id"] in interactions:
 | 
				
			||||||
 | 
					                interaction_type = interaction.data["custom_id"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                player = app_db.db_session.query(
 | 
				
			||||||
 | 
					                    Player
 | 
				
			||||||
 | 
					                ).where(
 | 
				
			||||||
 | 
					                    Player.discord_id == interaction.user.id
 | 
				
			||||||
 | 
					                ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if not player:
 | 
				
			||||||
 | 
					                    await interaction.response.send_message(
 | 
				
			||||||
 | 
					                        "This Discord account is not linked to a player. " +
 | 
				
			||||||
 | 
					                        "Contact <@195789918474207233> to link your account.",
 | 
				
			||||||
 | 
					                        ephemeral=True
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # log the interaction
 | 
				
			||||||
 | 
					                    user = await self.fetch_user(195789918474207233)
 | 
				
			||||||
 | 
					                    if user:
 | 
				
			||||||
 | 
					                        await user.send(
 | 
				
			||||||
 | 
					                            f"User <@{interaction.user.id}> tried to " +
 | 
				
			||||||
 | 
					                            "interact with an event but their account is " +
 | 
				
			||||||
 | 
					                            "not linked to a player."
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                event = app_db.db_session.query(
 | 
				
			||||||
 | 
					                    Event
 | 
				
			||||||
 | 
					                ).where(
 | 
				
			||||||
 | 
					                    Event.discord_message_id == interaction.message.id
 | 
				
			||||||
 | 
					                ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if event and player:
 | 
				
			||||||
 | 
					                    if interaction_type == "click_edit_event":
 | 
				
			||||||
 | 
					                        await interaction.response.send_modal(EventModal(event))
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        await handle_update_attendance(player, event, interaction, interaction_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					intents = discord.Intents.default()
 | 
				
			||||||
 | 
					client = Client(command_prefix="!", intents=intents)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@client.tree.command(
 | 
				
			||||||
 | 
					    name="setup-announcement-webhook",
 | 
				
			||||||
 | 
					    description="Set up announcements webhook in this channel",
 | 
				
			||||||
 | 
					    guild=guild_id
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@discord.app_commands.guild_only()
 | 
				
			||||||
 | 
					@discord.app_commands.default_permissions(manage_webhooks=True)
 | 
				
			||||||
 | 
					async def setup_announcements(interaction: discord.Interaction):
 | 
				
			||||||
 | 
					    await interaction.response.send_message(
 | 
				
			||||||
 | 
					        "Setting up announcement webhook. Any existing webhooks madde by " +
 | 
				
			||||||
 | 
					        "this command will be deleted.",
 | 
				
			||||||
 | 
					        ephemeral=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel = interaction.channel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert isinstance(channel, discord.TextChannel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for webhook in await channel.webhooks():
 | 
				
			||||||
 | 
					        if webhook.user == client.user:
 | 
				
			||||||
 | 
					            await webhook.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    webhook = await channel.create_webhook(name="availabili.tf webhook")
 | 
				
			||||||
 | 
					    content = (
 | 
				
			||||||
 | 
					        f"Webhook created: {webhook.url}\n" + 
 | 
				
			||||||
 | 
					        "Use this webhook URL in the Discord integration settings of your " +
 | 
				
			||||||
 | 
					        "team to receive interactive announcements."
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    await interaction.followup.send(content, ephemeral=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					client.run(discord_token)
 | 
				
			||||||
| 
						 | 
					@ -25,11 +25,12 @@ class GetUserResponse(PlayerSchema):
 | 
				
			||||||
    real_user: PlayerSchema | None
 | 
					    real_user: PlayerSchema | None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def from_model(cls, model: Player):
 | 
					    def from_model(cls, player: Player):
 | 
				
			||||||
        return GetUserResponse(
 | 
					        return GetUserResponse(
 | 
				
			||||||
            steam_id=str(model.steam_id),
 | 
					            steam_id=str(player.steam_id),
 | 
				
			||||||
            username=model.username,
 | 
					            username=player.username,
 | 
				
			||||||
            is_admin=model.is_admin,
 | 
					            is_admin=player.is_admin,
 | 
				
			||||||
 | 
					            discord_id=str(player.discord_id),
 | 
				
			||||||
            real_user=None,
 | 
					            real_user=None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,12 +45,9 @@ class GetUserResponse(PlayerSchema):
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
def get_user(player: Player, auth_session: AuthSession):
 | 
					def get_user(player: Player, auth_session: AuthSession):
 | 
				
			||||||
    if auth_session.player.steam_id != player.steam_id:
 | 
					    if auth_session.player.steam_id != player.steam_id:
 | 
				
			||||||
        return GetUserResponse(
 | 
					        response = GetUserResponse.from_model(player)
 | 
				
			||||||
            steam_id=str(player.steam_id),
 | 
					        response.real_user = PlayerSchema.from_model(auth_session.player)
 | 
				
			||||||
            username=player.username,
 | 
					        return response.dict(by_alias=True)
 | 
				
			||||||
            is_admin=player.is_admin,
 | 
					 | 
				
			||||||
            real_user=PlayerSchema.from_model(auth_session.player)
 | 
					 | 
				
			||||||
        ).dict(by_alias=True)
 | 
					 | 
				
			||||||
    return GetUserResponse.from_model(player).dict(by_alias=True)
 | 
					    return GetUserResponse.from_model(player).dict(by_alias=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_login.post("/authenticate")
 | 
					@api_login.post("/authenticate")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					"""Make players.discord_id unique
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Revision ID: 251a8108d7ff
 | 
				
			||||||
 | 
					Revises: f4330df94c44
 | 
				
			||||||
 | 
					Create Date: 2025-05-14 16:44:16.817690
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from alembic import op
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# revision identifiers, used by Alembic.
 | 
				
			||||||
 | 
					revision = '251a8108d7ff'
 | 
				
			||||||
 | 
					down_revision = 'f4330df94c44'
 | 
				
			||||||
 | 
					branch_labels = None
 | 
				
			||||||
 | 
					depends_on = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def upgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.create_unique_constraint("uq_players_discord_id", ['discord_id'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def downgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.drop_constraint("uq_players_discord_id", type_='unique')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					"""Add uniqueness constraints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Revision ID: 8a37924fd1be
 | 
				
			||||||
 | 
					Revises: 251a8108d7ff
 | 
				
			||||||
 | 
					Create Date: 2025-05-15 09:52:01.887077
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from alembic import op
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# revision identifiers, used by Alembic.
 | 
				
			||||||
 | 
					revision = '8a37924fd1be'
 | 
				
			||||||
 | 
					down_revision = '251a8108d7ff'
 | 
				
			||||||
 | 
					branch_labels = None
 | 
				
			||||||
 | 
					depends_on = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def upgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('events', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.create_unique_constraint('uq_events_discord_message_id', ['discord_message_id'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def downgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('events', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.drop_constraint('uq_events_discord_message_id', type_='unique')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					"""Add players.discord_id field
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Revision ID: f4330df94c44
 | 
				
			||||||
 | 
					Revises: f8588cdf998e
 | 
				
			||||||
 | 
					Create Date: 2025-05-14 14:06:29.368594
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from alembic import op
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# revision identifiers, used by Alembic.
 | 
				
			||||||
 | 
					revision = 'f4330df94c44'
 | 
				
			||||||
 | 
					down_revision = 'f8588cdf998e'
 | 
				
			||||||
 | 
					branch_labels = None
 | 
				
			||||||
 | 
					depends_on = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def upgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.add_column(sa.Column('discord_id', sa.BigInteger(), nullable=True))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def downgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.drop_column('discord_id')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,14 @@
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
import threading
 | 
					import threading
 | 
				
			||||||
from sqlalchemy.orm import mapped_column, relationship
 | 
					from sqlalchemy.orm import mapped_column, relationship, scoped_session
 | 
				
			||||||
from sqlalchemy.orm.attributes import Mapped
 | 
					from sqlalchemy.orm.attributes import Mapped
 | 
				
			||||||
from sqlalchemy.orm.properties import ForeignKey
 | 
					from sqlalchemy.orm.properties import ForeignKey
 | 
				
			||||||
 | 
					from sqlalchemy.orm.session import Session
 | 
				
			||||||
from sqlalchemy.schema import UniqueConstraint
 | 
					from sqlalchemy.schema import UniqueConstraint
 | 
				
			||||||
from sqlalchemy.types import TIMESTAMP, BigInteger, Integer, String, Text
 | 
					from sqlalchemy.types import TIMESTAMP, BigInteger, Integer, String, Text
 | 
				
			||||||
from sqlalchemy.sql import func
 | 
					from sqlalchemy.sql import func
 | 
				
			||||||
from sqlalchemy_utc import UtcDateTime
 | 
					from sqlalchemy_utc import UtcDateTime
 | 
				
			||||||
from discord_webhook import DiscordWebhook
 | 
					 | 
				
			||||||
import app_db
 | 
					import app_db
 | 
				
			||||||
import spec
 | 
					import spec
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
| 
						 | 
					@ -26,7 +27,7 @@ class Event(app_db.BaseModel):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
 | 
					    description: Mapped[str | None] = mapped_column(Text, nullable=True)
 | 
				
			||||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
					    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
				
			||||||
    discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
 | 
					    discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, unique=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    team: Mapped["Team"] = relationship("Team", back_populates="events")
 | 
					    team: Mapped["Team"] = relationship("Team", back_populates="events")
 | 
				
			||||||
    players: Mapped[list["PlayerEvent"]] = relationship(
 | 
					    players: Mapped[list["PlayerEvent"]] = relationship(
 | 
				
			||||||
| 
						 | 
					@ -40,7 +41,7 @@ class Event(app_db.BaseModel):
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_maximum_matching(self):
 | 
					    def get_maximum_matching(self):
 | 
				
			||||||
        players_teams_roles = app_db.db.session.query(
 | 
					        players_teams_roles = app_db.db_session.query(
 | 
				
			||||||
            PlayerTeamRole
 | 
					            PlayerTeamRole
 | 
				
			||||||
        ).join(
 | 
					        ).join(
 | 
				
			||||||
            PlayerTeam
 | 
					            PlayerTeam
 | 
				
			||||||
| 
						 | 
					@ -89,7 +90,10 @@ class Event(app_db.BaseModel):
 | 
				
			||||||
            if player.role:
 | 
					            if player.role:
 | 
				
			||||||
                player_info += f"**{player.role.role.name}:** "
 | 
					                player_info += f"**{player.role.role.name}:** "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            player_info += f"{player.player.username}"
 | 
					            if player.player.discord_id:
 | 
				
			||||||
 | 
					                player_info += f"<@{player.player.discord_id}>"
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                player_info += f"{player.player.username}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if player.has_confirmed:
 | 
					            if player.has_confirmed:
 | 
				
			||||||
                player_info += " ✅"
 | 
					                player_info += " ✅"
 | 
				
			||||||
| 
						 | 
					@ -105,8 +109,6 @@ class Event(app_db.BaseModel):
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                ringers_needed_msg = f" **({ringers_needed} ringers needed)**"
 | 
					                ringers_needed_msg = f" **({ringers_needed} ringers needed)**"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        domain = os.environ.get("DOMAIN", "availabili.tf")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return "\n".join([
 | 
					        return "\n".join([
 | 
				
			||||||
            f"# {self.name}",
 | 
					            f"# {self.name}",
 | 
				
			||||||
            "",
 | 
					            "",
 | 
				
			||||||
| 
						 | 
					@ -115,47 +117,95 @@ class Event(app_db.BaseModel):
 | 
				
			||||||
            f"<t:{start_timestamp}:f>",
 | 
					            f"<t:{start_timestamp}:f>",
 | 
				
			||||||
            "\n".join(players_info),
 | 
					            "\n".join(players_info),
 | 
				
			||||||
            f"Maximum roles filled: {matchings}" + ringers_needed_msg,
 | 
					            f"Maximum roles filled: {matchings}" + ringers_needed_msg,
 | 
				
			||||||
            "",
 | 
					            #"",
 | 
				
			||||||
            "[Confirm attendance here]" +
 | 
					            #"[Confirm attendance here]" +
 | 
				
			||||||
                f"(https://{domain}/team/id/{self.team.id})",
 | 
					            #    f"(https://{domain}/team/id/{self.team.id})",
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_discord_message_components(self):
 | 
				
			||||||
 | 
					        domain = os.environ.get("DOMAIN", "availabili.tf")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "type": 10,
 | 
				
			||||||
 | 
					                "content": self.get_discord_content(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "type": 1,
 | 
				
			||||||
 | 
					                "components": [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "type": 2,
 | 
				
			||||||
 | 
					                        "label": "✅",
 | 
				
			||||||
 | 
					                        "style": 3,
 | 
				
			||||||
 | 
					                        "custom_id": "click_attending"
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "type": 2,
 | 
				
			||||||
 | 
					                        "label": "⌛",
 | 
				
			||||||
 | 
					                        "style": 2,
 | 
				
			||||||
 | 
					                        "custom_id": "click_pending"
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "type": 2,
 | 
				
			||||||
 | 
					                        "label": "❌",
 | 
				
			||||||
 | 
					                        "style": 2,
 | 
				
			||||||
 | 
					                        "custom_id": "click_not_attending"
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "type": 2,
 | 
				
			||||||
 | 
					                        "label": "Edit",
 | 
				
			||||||
 | 
					                        "style": 2,
 | 
				
			||||||
 | 
					                        "custom_id": "click_edit_event"
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "type": 2,
 | 
				
			||||||
 | 
					                        "label": "View in browser",
 | 
				
			||||||
 | 
					                        "style": 5,
 | 
				
			||||||
 | 
					                        "url": f"https://{domain}/team/id/{self.team_id}"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_or_create_webhook(self):
 | 
					    def get_or_create_webhook(self):
 | 
				
			||||||
        integration = app_db.db.session.query(
 | 
					        integration = app_db.db_session.query(
 | 
				
			||||||
            TeamDiscordIntegration
 | 
					            TeamDiscordIntegration
 | 
				
			||||||
        ).where(
 | 
					        ).where(
 | 
				
			||||||
            TeamDiscordIntegration.team_id == self.team_id
 | 
					            TeamDiscordIntegration.team_id == self.team_id
 | 
				
			||||||
        ).first()
 | 
					        ).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not integration:
 | 
					        if not integration:
 | 
				
			||||||
            return None
 | 
					            return None, ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.discord_message_id:
 | 
					        webhook = {
 | 
				
			||||||
            return DiscordWebhook(
 | 
					            "username": integration.webhook_bot_name,
 | 
				
			||||||
                integration.webhook_url,
 | 
					            "avatar_url": integration.webhook_bot_profile_picture,
 | 
				
			||||||
                id=str(self.discord_message_id),
 | 
					            "flags": 1 << 15,
 | 
				
			||||||
                username=integration.webhook_bot_name,
 | 
					        }
 | 
				
			||||||
                avatar_url=integration.webhook_bot_profile_picture,
 | 
					
 | 
				
			||||||
            )
 | 
					        return webhook, integration.webhook_url
 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return DiscordWebhook(
 | 
					 | 
				
			||||||
                integration.webhook_url,
 | 
					 | 
				
			||||||
                username=integration.webhook_bot_name,
 | 
					 | 
				
			||||||
                avatar_url=integration.webhook_bot_profile_picture,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_discord_message(self):
 | 
					    def update_discord_message(self):
 | 
				
			||||||
        webhook = self.get_or_create_webhook()
 | 
					        domain = os.environ.get("DOMAIN", "availabili.tf")
 | 
				
			||||||
 | 
					        webhook, webhook_url = self.get_or_create_webhook()
 | 
				
			||||||
        if webhook:
 | 
					        if webhook:
 | 
				
			||||||
            webhook.content = self.get_discord_content()
 | 
					            params = "?with_components=true&wait=true"
 | 
				
			||||||
            if webhook.id:
 | 
					            webhook["components"] = self.get_discord_message_components()
 | 
				
			||||||
 | 
					            if self.discord_message_id:
 | 
				
			||||||
                # fire and forget
 | 
					                # fire and forget
 | 
				
			||||||
                threading.Thread(target=webhook.edit).start()
 | 
					                #threading.Thread(target=webhook.edit).start()
 | 
				
			||||||
 | 
					                del webhook["username"]
 | 
				
			||||||
 | 
					                del webhook["avatar_url"]
 | 
				
			||||||
 | 
					                webhook_url += f"/messages/{self.discord_message_id}"
 | 
				
			||||||
 | 
					                requests.patch(webhook_url + params, json=webhook)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                webhook.execute()
 | 
					                #webhook.execute()
 | 
				
			||||||
                if webhook_id := webhook.id:
 | 
					                response = requests.post(webhook_url + params, json=webhook)
 | 
				
			||||||
 | 
					                response = response.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if webhook_id := response["id"]:
 | 
				
			||||||
                    self.discord_message_id = int(webhook_id)
 | 
					                    self.discord_message_id = int(webhook_id)
 | 
				
			||||||
                    app_db.db.session.commit()
 | 
					                    app_db.db_session.commit()
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    raise Exception("Failed to create webhook")
 | 
					                    raise Exception("Failed to create webhook")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ class Player(app_db.BaseModel):
 | 
				
			||||||
    steam_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
 | 
					    steam_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
 | 
				
			||||||
    username: Mapped[str] = mapped_column(String(63))
 | 
					    username: Mapped[str] = mapped_column(String(63))
 | 
				
			||||||
    is_admin: Mapped[bool] = mapped_column(default=False)
 | 
					    is_admin: Mapped[bool] = mapped_column(default=False)
 | 
				
			||||||
 | 
					    discord_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, unique=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    teams: Mapped[list["PlayerTeam"]] = relationship(back_populates="player")
 | 
					    teams: Mapped[list["PlayerTeam"]] = relationship(back_populates="player")
 | 
				
			||||||
    auth_sessions: Mapped[list["AuthSession"]] = relationship(back_populates="player")
 | 
					    auth_sessions: Mapped[list["AuthSession"]] = relationship(back_populates="player")
 | 
				
			||||||
| 
						 | 
					@ -25,13 +26,15 @@ class PlayerSchema(spec.BaseModel):
 | 
				
			||||||
    steam_id: str
 | 
					    steam_id: str
 | 
				
			||||||
    username: str
 | 
					    username: str
 | 
				
			||||||
    is_admin: bool = False
 | 
					    is_admin: bool = False
 | 
				
			||||||
 | 
					    discord_id: str | None = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def from_model(cls, player: Player):
 | 
					    def from_model(cls, player: Player):
 | 
				
			||||||
        return cls(
 | 
					        return cls(
 | 
				
			||||||
            steam_id=str(player.steam_id),
 | 
					            steam_id=str(player.steam_id),
 | 
				
			||||||
            username=player.username,
 | 
					            username=player.username,
 | 
				
			||||||
            is_admin=player.is_admin
 | 
					            is_admin=player.is_admin,
 | 
				
			||||||
 | 
					            discord_id=str(player.discord_id) if player.discord_id else None
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ Flask-SQLAlchemy
 | 
				
			||||||
SQLAlchemy-Utc
 | 
					SQLAlchemy-Utc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# form/data validation
 | 
					# form/data validation
 | 
				
			||||||
pydantic==2.9.2
 | 
					pydantic==2.11.4
 | 
				
			||||||
spectree==1.4.1  # generates OpenAPI documents for us to make TypeScript API
 | 
					spectree==1.4.1  # generates OpenAPI documents for us to make TypeScript API
 | 
				
			||||||
                 # clients based on our pydantic models
 | 
					                 # clients based on our pydantic models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,9 @@ requests
 | 
				
			||||||
pytz  # timezone handling
 | 
					pytz  # timezone handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
discord-webhook  # for sending messages to Discord webhooks
 | 
					discord-webhook  # for sending messages to Discord webhooks
 | 
				
			||||||
 | 
					discord.py
 | 
				
			||||||
 | 
					https://github.com/HumanoidSandvichDispenser/Clyde/releases/download/test/discord_clyde-0.2.0-py3-none-any.whl
 | 
				
			||||||
 | 
					#discord-clyde
 | 
				
			||||||
 | 
					
 | 
				
			||||||
celery[redis]
 | 
					celery[redis]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,6 @@
 | 
				
			||||||
from flask import Blueprint, abort, make_response
 | 
					import os
 | 
				
			||||||
 | 
					from flask import Blueprint, abort, make_response, request
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
from spectree import Response
 | 
					from spectree import Response
 | 
				
			||||||
from middleware import requires_admin, requires_authentication
 | 
					from middleware import requires_admin, requires_authentication
 | 
				
			||||||
from models.player import Player, PlayerSchema
 | 
					from models.player import Player, PlayerSchema
 | 
				
			||||||
| 
						 | 
					@ -8,6 +10,11 @@ from app_db import db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
api_user = Blueprint("user", __name__, url_prefix="/user")
 | 
					api_user = Blueprint("user", __name__, url_prefix="/user")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: use env vars
 | 
				
			||||||
 | 
					DISCORD_TOKEN_URL = "https://discord.com/api/oauth2/token"
 | 
				
			||||||
 | 
					DISCORD_CLIENT_ID = "1372254613692219392" #os.getenv("DISCORD_CLIENT_ID")
 | 
				
			||||||
 | 
					DISCORD_CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SetUsernameJson(BaseModel):
 | 
					class SetUsernameJson(BaseModel):
 | 
				
			||||||
    username: str
 | 
					    username: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,3 +85,34 @@ def unset_doas(**_):
 | 
				
			||||||
    resp = make_response({ }, 204)
 | 
					    resp = make_response({ }, 204)
 | 
				
			||||||
    resp.delete_cookie("doas", httponly=True)
 | 
					    resp.delete_cookie("doas", httponly=True)
 | 
				
			||||||
    return resp
 | 
					    return resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#class DiscordAuthQuery(BaseModel):
 | 
				
			||||||
 | 
					#    code: str
 | 
				
			||||||
 | 
					#    redirect_uri: str
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#@api_user.post("/discord-authenticate")
 | 
				
			||||||
 | 
					#@spec.validate(
 | 
				
			||||||
 | 
					#    operation_id="discord_authenticate"
 | 
				
			||||||
 | 
					#)
 | 
				
			||||||
 | 
					#@requires_authentication
 | 
				
			||||||
 | 
					#def discord_authenticate(query: DiscordAuthQuery, player: Player, **_):
 | 
				
			||||||
 | 
					#    if not DISCORD_CLIENT_ID or not DISCORD_CLIENT_SECRET:
 | 
				
			||||||
 | 
					#        abort(500, "The site is not configured to use Discord authentication")
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#    data = {
 | 
				
			||||||
 | 
					#        "client_id": DISCORD_CLIENT_ID,
 | 
				
			||||||
 | 
					#        "client_secret": DISCORD_CLIENT_SECRET,
 | 
				
			||||||
 | 
					#        "grant_type": "authorization_code",
 | 
				
			||||||
 | 
					#        "code": query.code,
 | 
				
			||||||
 | 
					#        "redirect_uri": query.redirect_uri,
 | 
				
			||||||
 | 
					#        "scope": "identify"
 | 
				
			||||||
 | 
					#    }
 | 
				
			||||||
 | 
					#    response = requests.post(DISCORD_TOKEN_URL, data)
 | 
				
			||||||
 | 
					#    access_token = response.json()["access_token"]
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#    headers = {
 | 
				
			||||||
 | 
					#        "authorization": f"Bearer {access_token}"
 | 
				
			||||||
 | 
					#    }
 | 
				
			||||||
 | 
					#    response = requests.get("https://discord.com/api/v10/users/@me", headers=headers)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#    id = response.json()[]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,8 @@ services:
 | 
				
			||||||
      - ./backend-flask:/app
 | 
					      - ./backend-flask:/app
 | 
				
			||||||
    networks:
 | 
					    networks:
 | 
				
			||||||
      - prod-network
 | 
					      - prod-network
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - FLASK_DEBUG=0
 | 
					      - FLASK_DEBUG=0
 | 
				
			||||||
      - FLASK_CELERY_BROKER_URL=redis://redis:6379/0
 | 
					      - FLASK_CELERY_BROKER_URL=redis://redis:6379/0
 | 
				
			||||||
| 
						 | 
					@ -43,7 +45,7 @@ services:
 | 
				
			||||||
  # ETL job (runs with the same source as the backend)
 | 
					  # ETL job (runs with the same source as the backend)
 | 
				
			||||||
  celery-worker:
 | 
					  celery-worker:
 | 
				
			||||||
    container_name: worker-production
 | 
					    container_name: worker-production
 | 
				
			||||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
 | 
					    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
 | 
				
			||||||
    image: backend-flask-production
 | 
					    image: backend-flask-production
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
					      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
				
			||||||
| 
						 | 
					@ -53,10 +55,24 @@ services:
 | 
				
			||||||
      - ./backend-flask:/app
 | 
					      - ./backend-flask:/app
 | 
				
			||||||
    networks:
 | 
					    networks:
 | 
				
			||||||
      - prod-network
 | 
					      - prod-network
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
      - db
 | 
					      - db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # discord bot
 | 
				
			||||||
 | 
					  discord-bot:
 | 
				
			||||||
 | 
					    container_name: discord-bot
 | 
				
			||||||
 | 
					    command: python discord_bot.py
 | 
				
			||||||
 | 
					    image: backend-flask-production
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./backend-flask:/app
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      - prod-network
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # message broker
 | 
					  # message broker
 | 
				
			||||||
  redis:
 | 
					  redis:
 | 
				
			||||||
    image: redis:alpine
 | 
					    image: redis:alpine
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ services:
 | 
				
			||||||
  # ETL job (runs with the same source as the backend)
 | 
					  # ETL job (runs with the same source as the backend)
 | 
				
			||||||
  celery-worker:
 | 
					  celery-worker:
 | 
				
			||||||
    container_name: worker
 | 
					    container_name: worker
 | 
				
			||||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
 | 
					    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
					      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
				
			||||||
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
					      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
				
			||||||
| 
						 | 
					@ -35,6 +35,20 @@ services:
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # discord bot
 | 
				
			||||||
 | 
					  discord-bot:
 | 
				
			||||||
 | 
					    container_name: discord-bot
 | 
				
			||||||
 | 
					    command: python discord_bot.py
 | 
				
			||||||
 | 
					    image: backend-flask
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./backend-flask:/app
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      - dev-network
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - DATABASE_URI=sqlite:///instance/db.sqlite3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # message broker
 | 
					  # message broker
 | 
				
			||||||
  redis:
 | 
					  redis:
 | 
				
			||||||
    image: redis:alpine
 | 
					    image: redis:alpine
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue