Compare commits
	
		
			No commits in common. "00e1cedfcd6489796e3a59d0ebebe26be0ec5d4d" and "c05794ad99f07ac8db4bf6d52fd80216dfd65718" have entirely different histories. 
		
	
	
		
			00e1cedfcd
			...
			c05794ad99
		
	
		
	| 
						 | 
				
			
			@ -4,7 +4,6 @@
 | 
			
		|||
/* eslint-disable */
 | 
			
		||||
import type { PlayerSchema } from './PlayerSchema';
 | 
			
		||||
export type GetUserResponse = {
 | 
			
		||||
    discordId?: (string | null);
 | 
			
		||||
    isAdmin?: boolean;
 | 
			
		||||
    realUser: (PlayerSchema | null);
 | 
			
		||||
    steamId: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
export type PlayerSchema = {
 | 
			
		||||
    discordId?: (string | null);
 | 
			
		||||
    isAdmin?: boolean;
 | 
			
		||||
    steamId: string;
 | 
			
		||||
    username: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import type { RoleSchema } from './RoleSchema';
 | 
			
		|||
export type ViewTeamMembersResponse = {
 | 
			
		||||
    availability: Array<number>;
 | 
			
		||||
    createdAt: string;
 | 
			
		||||
    discordId?: (string | null);
 | 
			
		||||
    isAdmin?: boolean;
 | 
			
		||||
    isTeamLeader?: boolean;
 | 
			
		||||
    playtime: number;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,6 @@ export const useAuthStore = defineStore("auth", () => {
 | 
			
		|||
  const hasCheckedAuth = ref(false);
 | 
			
		||||
  const isAdmin = ref(false);
 | 
			
		||||
  const realUser = ref<PlayerSchema | null>(null);
 | 
			
		||||
  const discordId = ref<string | null>("");
 | 
			
		||||
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +39,6 @@ export const useAuthStore = defineStore("auth", () => {
 | 
			
		|||
        user.value = response;
 | 
			
		||||
        isAdmin.value = response.isAdmin || (response.realUser?.isAdmin ?? false);
 | 
			
		||||
        realUser.value = response.realUser ?? null;
 | 
			
		||||
        discordId.value = response.discordId ?? "";
 | 
			
		||||
 | 
			
		||||
        return response;
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +116,6 @@ export const useAuthStore = defineStore("auth", () => {
 | 
			
		|||
    username,
 | 
			
		||||
    isAdmin,
 | 
			
		||||
    realUser,
 | 
			
		||||
    discordId,
 | 
			
		||||
    isLoggedIn,
 | 
			
		||||
    hasCheckedAuth,
 | 
			
		||||
    isRegistering,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,6 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
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";
 | 
			
		||||
});
 | 
			
		||||
import { onMounted, ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const displayName = ref("");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,29 +25,6 @@ onMounted(() => {
 | 
			
		|||
        </h3>
 | 
			
		||||
        <input v-model="displayName" />
 | 
			
		||||
      </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="action-buttons">
 | 
			
		||||
          <button class="accent" @click="save">Save</button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
# Use an official Python runtime as a parent image
 | 
			
		||||
FROM python:3.13-slim
 | 
			
		||||
FROM python:3.12-slim
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt /
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,7 @@ from flask import Flask
 | 
			
		|||
from flask_migrate import Migrate
 | 
			
		||||
from flask_sqlalchemy import SQLAlchemy
 | 
			
		||||
from sqlalchemy import MetaData
 | 
			
		||||
from sqlalchemy.engine import create_engine
 | 
			
		||||
from sqlalchemy.orm import DeclarativeBase, scoped_session, sessionmaker
 | 
			
		||||
from sqlalchemy.orm import DeclarativeBase
 | 
			
		||||
from celery import Celery, Task
 | 
			
		||||
 | 
			
		||||
class BaseModel(DeclarativeBase):
 | 
			
		||||
| 
						 | 
				
			
			@ -75,24 +74,7 @@ metadata = MetaData(naming_convention=convention)
 | 
			
		|||
def create_db() -> SQLAlchemy:
 | 
			
		||||
    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()
 | 
			
		||||
#db = SQLAlchemy(model_class=BaseModel, metadata=metadata)
 | 
			
		||||
db = create_db()
 | 
			
		||||
db_session: scoped_session = db.session
 | 
			
		||||
migrate = Migrate(app, db, render_as_batch=True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,192 +0,0 @@
 | 
			
		|||
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,12 +25,11 @@ class GetUserResponse(PlayerSchema):
 | 
			
		|||
    real_user: PlayerSchema | None
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_model(cls, player: Player):
 | 
			
		||||
    def from_model(cls, model: Player):
 | 
			
		||||
        return GetUserResponse(
 | 
			
		||||
            steam_id=str(player.steam_id),
 | 
			
		||||
            username=player.username,
 | 
			
		||||
            is_admin=player.is_admin,
 | 
			
		||||
            discord_id=str(player.discord_id),
 | 
			
		||||
            steam_id=str(model.steam_id),
 | 
			
		||||
            username=model.username,
 | 
			
		||||
            is_admin=model.is_admin,
 | 
			
		||||
            real_user=None,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -45,9 +44,12 @@ class GetUserResponse(PlayerSchema):
 | 
			
		|||
@requires_authentication
 | 
			
		||||
def get_user(player: Player, auth_session: AuthSession):
 | 
			
		||||
    if auth_session.player.steam_id != player.steam_id:
 | 
			
		||||
        response = GetUserResponse.from_model(player)
 | 
			
		||||
        response.real_user = PlayerSchema.from_model(auth_session.player)
 | 
			
		||||
        return response.dict(by_alias=True)
 | 
			
		||||
        return GetUserResponse(
 | 
			
		||||
            steam_id=str(player.steam_id),
 | 
			
		||||
            username=player.username,
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
@api_login.post("/authenticate")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,32 +0,0 @@
 | 
			
		|||
"""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 ###
 | 
			
		||||
| 
						 | 
				
			
			@ -1,32 +0,0 @@
 | 
			
		|||
"""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 ###
 | 
			
		||||
| 
						 | 
				
			
			@ -1,32 +0,0 @@
 | 
			
		|||
"""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,14 +1,13 @@
 | 
			
		|||
from datetime import datetime
 | 
			
		||||
import requests
 | 
			
		||||
import threading
 | 
			
		||||
from sqlalchemy.orm import mapped_column, relationship, scoped_session
 | 
			
		||||
from sqlalchemy.orm import mapped_column, relationship
 | 
			
		||||
from sqlalchemy.orm.attributes import Mapped
 | 
			
		||||
from sqlalchemy.orm.properties import ForeignKey
 | 
			
		||||
from sqlalchemy.orm.session import Session
 | 
			
		||||
from sqlalchemy.schema import UniqueConstraint
 | 
			
		||||
from sqlalchemy.types import TIMESTAMP, BigInteger, Integer, String, Text
 | 
			
		||||
from sqlalchemy.sql import func
 | 
			
		||||
from sqlalchemy_utc import UtcDateTime
 | 
			
		||||
from discord_webhook import DiscordWebhook
 | 
			
		||||
import app_db
 | 
			
		||||
import spec
 | 
			
		||||
import os
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +26,7 @@ class Event(app_db.BaseModel):
 | 
			
		|||
 | 
			
		||||
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
 | 
			
		||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
			
		||||
    discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, unique=True)
 | 
			
		||||
    discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
 | 
			
		||||
 | 
			
		||||
    team: Mapped["Team"] = relationship("Team", back_populates="events")
 | 
			
		||||
    players: Mapped[list["PlayerEvent"]] = relationship(
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +40,7 @@ class Event(app_db.BaseModel):
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
    def get_maximum_matching(self):
 | 
			
		||||
        players_teams_roles = app_db.db_session.query(
 | 
			
		||||
        players_teams_roles = app_db.db.session.query(
 | 
			
		||||
            PlayerTeamRole
 | 
			
		||||
        ).join(
 | 
			
		||||
            PlayerTeam
 | 
			
		||||
| 
						 | 
				
			
			@ -90,9 +89,6 @@ class Event(app_db.BaseModel):
 | 
			
		|||
            if player.role:
 | 
			
		||||
                player_info += f"**{player.role.role.name}:** "
 | 
			
		||||
 | 
			
		||||
            if player.player.discord_id:
 | 
			
		||||
                player_info += f"<@{player.player.discord_id}>"
 | 
			
		||||
            else:
 | 
			
		||||
            player_info += f"{player.player.username}"
 | 
			
		||||
 | 
			
		||||
            if player.has_confirmed:
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +105,8 @@ class Event(app_db.BaseModel):
 | 
			
		|||
            else:
 | 
			
		||||
                ringers_needed_msg = f" **({ringers_needed} ringers needed)**"
 | 
			
		||||
 | 
			
		||||
        domain = os.environ.get("DOMAIN", "availabili.tf")
 | 
			
		||||
 | 
			
		||||
        return "\n".join([
 | 
			
		||||
            f"# {self.name}",
 | 
			
		||||
            "",
 | 
			
		||||
| 
						 | 
				
			
			@ -117,95 +115,47 @@ class Event(app_db.BaseModel):
 | 
			
		|||
            f"<t:{start_timestamp}:f>",
 | 
			
		||||
            "\n".join(players_info),
 | 
			
		||||
            f"Maximum roles filled: {matchings}" + ringers_needed_msg,
 | 
			
		||||
            #"",
 | 
			
		||||
            #"[Confirm attendance here]" +
 | 
			
		||||
            #    f"(https://{domain}/team/id/{self.team.id})",
 | 
			
		||||
            "",
 | 
			
		||||
            "[Confirm attendance here]" +
 | 
			
		||||
                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):
 | 
			
		||||
        integration = app_db.db_session.query(
 | 
			
		||||
        integration = app_db.db.session.query(
 | 
			
		||||
            TeamDiscordIntegration
 | 
			
		||||
        ).where(
 | 
			
		||||
            TeamDiscordIntegration.team_id == self.team_id
 | 
			
		||||
        ).first()
 | 
			
		||||
 | 
			
		||||
        if not integration:
 | 
			
		||||
            return None, ""
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        webhook = {
 | 
			
		||||
            "username": integration.webhook_bot_name,
 | 
			
		||||
            "avatar_url": integration.webhook_bot_profile_picture,
 | 
			
		||||
            "flags": 1 << 15,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return webhook, integration.webhook_url
 | 
			
		||||
        if self.discord_message_id:
 | 
			
		||||
            return DiscordWebhook(
 | 
			
		||||
                integration.webhook_url,
 | 
			
		||||
                id=str(self.discord_message_id),
 | 
			
		||||
                username=integration.webhook_bot_name,
 | 
			
		||||
                avatar_url=integration.webhook_bot_profile_picture,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return DiscordWebhook(
 | 
			
		||||
                integration.webhook_url,
 | 
			
		||||
                username=integration.webhook_bot_name,
 | 
			
		||||
                avatar_url=integration.webhook_bot_profile_picture,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def update_discord_message(self):
 | 
			
		||||
        domain = os.environ.get("DOMAIN", "availabili.tf")
 | 
			
		||||
        webhook, webhook_url = self.get_or_create_webhook()
 | 
			
		||||
        webhook = self.get_or_create_webhook()
 | 
			
		||||
        if webhook:
 | 
			
		||||
            params = "?with_components=true&wait=true"
 | 
			
		||||
            webhook["components"] = self.get_discord_message_components()
 | 
			
		||||
            if self.discord_message_id:
 | 
			
		||||
            webhook.content = self.get_discord_content()
 | 
			
		||||
            if webhook.id:
 | 
			
		||||
                # fire and forget
 | 
			
		||||
                #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)
 | 
			
		||||
                threading.Thread(target=webhook.edit).start()
 | 
			
		||||
            else:
 | 
			
		||||
                #webhook.execute()
 | 
			
		||||
                response = requests.post(webhook_url + params, json=webhook)
 | 
			
		||||
                response = response.json()
 | 
			
		||||
 | 
			
		||||
                if webhook_id := response["id"]:
 | 
			
		||||
                webhook.execute()
 | 
			
		||||
                if webhook_id := webhook.id:
 | 
			
		||||
                    self.discord_message_id = int(webhook_id)
 | 
			
		||||
                    app_db.db_session.commit()
 | 
			
		||||
                    app_db.db.session.commit()
 | 
			
		||||
                else:
 | 
			
		||||
                    raise Exception("Failed to create webhook")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,6 @@ class Player(app_db.BaseModel):
 | 
			
		|||
    steam_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
 | 
			
		||||
    username: Mapped[str] = mapped_column(String(63))
 | 
			
		||||
    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")
 | 
			
		||||
    auth_sessions: Mapped[list["AuthSession"]] = relationship(back_populates="player")
 | 
			
		||||
| 
						 | 
				
			
			@ -26,15 +25,13 @@ class PlayerSchema(spec.BaseModel):
 | 
			
		|||
    steam_id: str
 | 
			
		||||
    username: str
 | 
			
		||||
    is_admin: bool = False
 | 
			
		||||
    discord_id: str | None = None
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_model(cls, player: Player):
 | 
			
		||||
        return cls(
 | 
			
		||||
            steam_id=str(player.steam_id),
 | 
			
		||||
            username=player.username,
 | 
			
		||||
            is_admin=player.is_admin,
 | 
			
		||||
            discord_id=str(player.discord_id) if player.discord_id else None
 | 
			
		||||
            is_admin=player.is_admin
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ Flask-SQLAlchemy
 | 
			
		|||
SQLAlchemy-Utc
 | 
			
		||||
 | 
			
		||||
# form/data validation
 | 
			
		||||
pydantic==2.11.4
 | 
			
		||||
pydantic==2.9.2
 | 
			
		||||
spectree==1.4.1  # generates OpenAPI documents for us to make TypeScript API
 | 
			
		||||
                 # clients based on our pydantic models
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -22,9 +22,6 @@ requests
 | 
			
		|||
pytz  # timezone handling
 | 
			
		||||
 | 
			
		||||
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]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,4 @@
 | 
			
		|||
import os
 | 
			
		||||
from flask import Blueprint, abort, make_response, request
 | 
			
		||||
import requests
 | 
			
		||||
from flask import Blueprint, abort, make_response
 | 
			
		||||
from spectree import Response
 | 
			
		||||
from middleware import requires_admin, requires_authentication
 | 
			
		||||
from models.player import Player, PlayerSchema
 | 
			
		||||
| 
						 | 
				
			
			@ -10,11 +8,6 @@ from app_db import db
 | 
			
		|||
 | 
			
		||||
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):
 | 
			
		||||
    username: str
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,34 +78,3 @@ def unset_doas(**_):
 | 
			
		|||
    resp = make_response({ }, 204)
 | 
			
		||||
    resp.delete_cookie("doas", httponly=True)
 | 
			
		||||
    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,8 +30,6 @@ services:
 | 
			
		|||
      - ./backend-flask:/app
 | 
			
		||||
    networks:
 | 
			
		||||
      - prod-network
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
    environment:
 | 
			
		||||
      - FLASK_DEBUG=0
 | 
			
		||||
      - FLASK_CELERY_BROKER_URL=redis://redis:6379/0
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +43,7 @@ services:
 | 
			
		|||
  # ETL job (runs with the same source as the backend)
 | 
			
		||||
  celery-worker:
 | 
			
		||||
    container_name: worker-production
 | 
			
		||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
 | 
			
		||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
 | 
			
		||||
    image: backend-flask-production
 | 
			
		||||
    environment:
 | 
			
		||||
      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
			
		||||
| 
						 | 
				
			
			@ -55,24 +53,10 @@ services:
 | 
			
		|||
      - ./backend-flask:/app
 | 
			
		||||
    networks:
 | 
			
		||||
      - prod-network
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - redis
 | 
			
		||||
      - 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
 | 
			
		||||
  redis:
 | 
			
		||||
    image: redis:alpine
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@ services:
 | 
			
		|||
  # ETL job (runs with the same source as the backend)
 | 
			
		||||
  celery-worker:
 | 
			
		||||
    container_name: worker
 | 
			
		||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
 | 
			
		||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
 | 
			
		||||
    environment:
 | 
			
		||||
      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
			
		||||
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
			
		||||
| 
						 | 
				
			
			@ -35,20 +35,6 @@ services:
 | 
			
		|||
    depends_on:
 | 
			
		||||
      - 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
 | 
			
		||||
  redis:
 | 
			
		||||
    image: redis:alpine
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue