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