From 3394f2271e127e1c1fc0501c664ba7d4606744a2 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Tue, 19 Nov 2024 16:34:51 -0800 Subject: [PATCH] refactor: Split team routes The team routes have been split into separate blueprints for better modularity and maintainability. The team invite and team integration routes are now handled by their respective blueprints. --- backend-flask/team.py | 297 +----------------------------- backend-flask/team_integration.py | 145 +++++++++++++++ backend-flask/team_invite.py | 158 ++++++++++++++++ 3 files changed, 312 insertions(+), 288 deletions(-) create mode 100644 backend-flask/team_integration.py create mode 100644 backend-flask/team_invite.py diff --git a/backend-flask/team.py b/backend-flask/team.py index 033d155..bb22c0a 100644 --- a/backend-flask/team.py +++ b/backend-flask/team.py @@ -1,27 +1,25 @@ -from datetime import UTC, datetime, timedelta, timezone -from random import randint, random -import sys -import time -from typing import List, cast -from flask import Blueprint, abort, jsonify, make_response, request +from datetime import datetime, timedelta, timezone +from typing import cast +from flask import Blueprint, abort, make_response from pydantic.v1 import validator from spectree import Response -from sqlalchemy.orm import joinedload, subqueryload +from sqlalchemy.orm import joinedload from app_db import db from models.player import Player, PlayerSchema from models.player_team import PlayerTeam from models.player_team_availability import PlayerTeamAvailability from models.player_team_role import PlayerTeamRole, RoleSchema from models.team import Team, TeamSchema -from models.team_invite import TeamInvite, TeamInviteSchema -from models.team_integration import AbstractTeamIntegrationSchema, TeamDiscordIntegration, TeamDiscordIntegrationSchema, TeamIntegration, TeamIntegrationSchema -from middleware import assert_team_authority, requires_authentication, requires_team_membership -import models +from middleware import requires_authentication from spec import spec, BaseModel +from team_invite import api_team_invite +from team_integration import api_team_integration import pytz api_team = Blueprint("team", __name__, url_prefix="/team") +api_team.register_blueprint(api_team_invite) +api_team.register_blueprint(api_team_integration) def map_player_to_schema(player: Player): return PlayerSchema( @@ -419,280 +417,3 @@ def edit_member_roles( db.session.commit() return make_response({ }, 204) - -@api_team.get("/id//invite") -@spec.validate( - resp=Response( - HTTP_200=list[TeamInviteSchema], - HTTP_404=None, - ), - operation_id="get_invites" -) -@requires_authentication -@requires_team_membership -def get_invites(team_id: int, **_): - invites = db.session.query( - TeamInvite - ).where( - TeamInvite.team_id == team_id - ).all() - - def map_invite_to_schema(invite: TeamInvite): - return TeamInviteSchema( - key=invite.key, - team_id=invite.team_id, - created_at=invite.created_at, - ).dict(by_alias=True) - - return list(map(map_invite_to_schema, invites)), 200 - -@api_team.post("/id//invite") -@spec.validate( - resp=Response( - HTTP_200=TeamInviteSchema, - HTTP_404=None, - ), - operation_id="create_invite" -) -@requires_authentication -@requires_team_membership -def create_invite(team_id: int, **_): - team_id_shifted = int(team_id) << 48 - random_value_shifted = int(randint(0, (1 << 16) - 1)) << 32 - timestamp = int(time.time()) & ((1 << 32) - 1) - - key_int = timestamp | team_id_shifted | random_value_shifted - key_hex = "%0.16X" % key_int - - invite = TeamInvite() - invite.team_id = team_id - invite.key = key_hex - - db.session.add(invite) - db.session.flush() - db.session.refresh(invite) - - response = TeamInviteSchema( - key=key_hex, - team_id=team_id, - created_at=invite.created_at - ) - - db.session.commit() - - return response.dict(by_alias=True), 200 - -@api_team.post("/id//consume-invite/") -@spec.validate( - resp=Response( - HTTP_204=None, - HTTP_404=None, - ), - operation_id="consume_invite" -) -@requires_authentication -def consume_invite(player: Player, team_id: int, key: str, **kwargs): - invite = db.session.query( - TeamInvite - ).where( - TeamInvite.team_id == team_id - ).where( - TeamInvite.key == key - ).one_or_none() - - if not invite: - abort(404) - - player_team = db.session.query( - PlayerTeam - ).where( - PlayerTeam.player_id == player.steam_id - ).where( - PlayerTeam.team_id == team_id - ).one_or_none() - - if player_team: - abort(409) - - player_team = PlayerTeam() - player_team.player = player - player_team.team_id = team_id - - db.session.add(player_team) - - if invite.delete_on_use: - db.session.delete(invite) - - db.session.commit() - - return make_response({ }, 204) - -@api_team.delete("/id//invite/") -@spec.validate( - resp=Response( - HTTP_204=None, - HTTP_404=None, - ), - operation_id="revoke_invite" -) -@requires_authentication -def revoke_invite(player: Player, team_id: int, key: str, **kwargs): - player_team = db.session.query( - PlayerTeam - ).where( - PlayerTeam.player_id == player.steam_id - ).where( - PlayerTeam.team_id == team_id - ).one_or_none() - - if not player_team: - abort(404) - - invite = db.session.query( - TeamInvite - ).where( - TeamInvite.team_id == team_id - ).where( - TeamInvite.key == key - ).one_or_none() - - if not invite: - abort(404) - - db.session.delete(invite) - db.session.commit() - return make_response({ }, 204) - -@api_team.get("/id//integrations") -@spec.validate( - resp=Response( - HTTP_200=list[TeamIntegrationSchema], - HTTP_404=None, - ), - operation_id="get_integrations" -) -@requires_authentication -def get_integrations(player: Player, team_id: int, **kwargs): - player_team = db.session.query( - PlayerTeam - ).where( - PlayerTeam.player_id == player.steam_id - ).where( - PlayerTeam.team_id == team_id - ).one_or_none() - - if not player_team: - abort(404) - - integrations = db.session.query( - TeamIntegration - ).where( - TeamIntegration.team_id == team_id - ).all() - - def map_integration_to_schema(integration: TeamIntegration): - return TeamIntegrationSchema.from_model( - integration - ).dict(by_alias=True) - - return list(map(map_integration_to_schema, integrations)) - -@api_team.post("/id//integrations/") -@spec.validate( - resp=Response( - HTTP_200=TeamIntegrationSchema, - ), - operation_id="create_integration" -) -@requires_authentication -@requires_team_membership -def create_integration(player_team: PlayerTeam, integration_type: str, **_): - assert_team_authority(player_team) - - if integration_type == "discord": - integration = TeamDiscordIntegration() - integration.team_id = player_team.team_id - integration.webhook_url = "" - else: - abort(404) - - db.session.add(integration) - db.session.commit() - - return TeamIntegrationSchema.from_model( - integration - ).dict(by_alias=True), 200 - -@api_team.delete("/id//integrations/") -@spec.validate( - resp=Response( - HTTP_204=None, - ), - operation_id="delete_integration" -) -@requires_authentication -@requires_team_membership -def delete_integration(player_team: PlayerTeam, integration_id: int, **_): - assert_team_authority(player_team) - - integration = db.session.query( - TeamIntegration - ).where( - TeamIntegration.team_id == player_team.team_id - ).where( - TeamIntegration.id == integration_id - ).one_or_none() - - if not integration: - abort(404) - - db.session.delete(integration) - db.session.commit() - - return make_response({ }, 204) - -@api_team.patch("/id//integrations/") -@spec.validate( - resp=Response( - HTTP_200=TeamIntegrationSchema, - ), - operation_id="update_integration" -) -@requires_authentication -@requires_team_membership -def update_integration( - player_team: PlayerTeam, - integration_id: int, - json: AbstractTeamIntegrationSchema, - **_ -): - assert_team_authority(player_team) - - integration = db.session.query( - TeamIntegration - ).where( - TeamIntegration.team_id == player_team.team_id - ).where( - TeamIntegration.id == integration_id - ).one_or_none() - - if not integration: - abort(404) - - if isinstance(integration, TeamDiscordIntegration): - print(json.dict(), file=sys.stderr) - if json.__root__.integration_type == "team_discord_integrations": - discord_integration = cast(TeamDiscordIntegration, json.__root__) - integration.webhook_url = discord_integration.webhook_url - #if isinstance(json, TeamDiscordIntegrationSchema): - # integration.webhook_url = json.webhook_url - else: - abort(400) - else: - abort(404) - - db.session.commit() - - return TeamIntegrationSchema.from_model( - integration - ).dict(by_alias=True), 200 diff --git a/backend-flask/team_integration.py b/backend-flask/team_integration.py new file mode 100644 index 0000000..eb16f75 --- /dev/null +++ b/backend-flask/team_integration.py @@ -0,0 +1,145 @@ +from flask import Blueprint, abort, make_response +from spectree import Response +from typing import cast + + +from app_db import db +from middleware import assert_team_authority, requires_authentication, requires_team_membership +from models.player import Player +from models.player_team import PlayerTeam +from models.team_integration import AbstractTeamIntegrationSchema, TeamDiscordIntegration, TeamIntegration, TeamIntegrationSchema +from spec import spec + + +api_team_integration = Blueprint("team_integration", __name__) + +@api_team_integration.get("/id//integrations") +@spec.validate( + resp=Response( + HTTP_200=list[TeamIntegrationSchema], + HTTP_404=None, + ), + operation_id="get_integrations" +) +@requires_authentication +def get_integrations(player: Player, team_id: int, **_): + player_team = db.session.query( + PlayerTeam + ).where( + PlayerTeam.player_id == player.steam_id + ).where( + PlayerTeam.team_id == team_id + ).one_or_none() + + if not player_team: + abort(404) + + integrations = db.session.query( + TeamIntegration + ).where( + TeamIntegration.team_id == team_id + ).all() + + def map_integration_to_schema(integration: TeamIntegration): + return TeamIntegrationSchema.from_model( + integration + ).dict(by_alias=True) + + return list(map(map_integration_to_schema, integrations)) + +@api_team_integration.post("/id//integrations/") +@spec.validate( + resp=Response( + HTTP_200=TeamIntegrationSchema, + ), + operation_id="create_integration" +) +@requires_authentication +@requires_team_membership +def create_integration(player_team: PlayerTeam, integration_type: str, **_): + assert_team_authority(player_team) + + if integration_type == "discord": + integration = TeamDiscordIntegration() + integration.team_id = player_team.team_id + integration.webhook_url = "" + else: + abort(404) + + db.session.add(integration) + db.session.commit() + + return TeamIntegrationSchema.from_model( + integration + ).dict(by_alias=True), 200 + +@api_team_integration.delete("/id//integrations/") +@spec.validate( + resp=Response( + HTTP_204=None, + ), + operation_id="delete_integration" +) +@requires_authentication +@requires_team_membership +def delete_integration(player_team: PlayerTeam, integration_id: int, **_): + assert_team_authority(player_team) + + integration = db.session.query( + TeamIntegration + ).where( + TeamIntegration.team_id == player_team.team_id + ).where( + TeamIntegration.id == integration_id + ).one_or_none() + + if not integration: + abort(404) + + db.session.delete(integration) + db.session.commit() + + return make_response({ }, 204) + +@api_team_integration.patch("/id//integrations/") +@spec.validate( + resp=Response( + HTTP_200=TeamIntegrationSchema, + ), + operation_id="update_integration" +) +@requires_authentication +@requires_team_membership +def update_integration( + player_team: PlayerTeam, + integration_id: int, + json: AbstractTeamIntegrationSchema, + **_ +): + assert_team_authority(player_team) + + integration = db.session.query( + TeamIntegration + ).where( + TeamIntegration.team_id == player_team.team_id + ).where( + TeamIntegration.id == integration_id + ).one_or_none() + + if not integration: + abort(404) + + if isinstance(integration, TeamDiscordIntegration): + if json.__root__.integration_type == "team_discord_integrations": + discord_integration = cast(TeamDiscordIntegration, json.__root__) + integration.webhook_url = discord_integration.webhook_url + else: + abort(400) + else: + abort(404) + + db.session.commit() + + return TeamIntegrationSchema.from_model( + integration + ).dict(by_alias=True), 200 diff --git a/backend-flask/team_invite.py b/backend-flask/team_invite.py new file mode 100644 index 0000000..786a981 --- /dev/null +++ b/backend-flask/team_invite.py @@ -0,0 +1,158 @@ +from random import randint +from flask import Blueprint, abort, make_response +from spectree import Response +import time + +from app_db import db +from middleware import requires_authentication, requires_team_membership +from models.player import Player +from models.player_team import PlayerTeam +from models.team_invite import TeamInvite, TeamInviteSchema +from spec import spec + + +api_team_invite = Blueprint("team_invite", __name__) + +@api_team_invite.get("/id//invite") +@spec.validate( + resp=Response( + HTTP_200=list[TeamInviteSchema], + HTTP_404=None, + ), + operation_id="get_invites" +) +@requires_authentication +@requires_team_membership +def get_invites(team_id: int, **_): + invites = db.session.query( + TeamInvite + ).where( + TeamInvite.team_id == team_id + ).all() + + def map_invite_to_schema(invite: TeamInvite): + return TeamInviteSchema( + key=invite.key, + team_id=invite.team_id, + created_at=invite.created_at, + ).dict(by_alias=True) + + return list(map(map_invite_to_schema, invites)), 200 + +@api_team_invite.post("/id//invite") +@spec.validate( + resp=Response( + HTTP_200=TeamInviteSchema, + HTTP_404=None, + ), + operation_id="create_invite" +) +@requires_authentication +@requires_team_membership +def create_invite(team_id: int, **_): + team_id_shifted = int(team_id) << 48 + random_value_shifted = int(randint(0, (1 << 16) - 1)) << 32 + timestamp = int(time.time()) & ((1 << 32) - 1) + + key_int = timestamp | team_id_shifted | random_value_shifted + key_hex = "%0.16X" % key_int + + invite = TeamInvite() + invite.team_id = team_id + invite.key = key_hex + + db.session.add(invite) + db.session.flush() + db.session.refresh(invite) + + response = TeamInviteSchema( + key=key_hex, + team_id=team_id, + created_at=invite.created_at + ) + + db.session.commit() + + return response.dict(by_alias=True), 200 + +@api_team_invite.post("/id//consume-invite/") +@spec.validate( + resp=Response( + HTTP_204=None, + HTTP_404=None, + ), + operation_id="consume_invite" +) +@requires_authentication +def consume_invite(player: Player, team_id: int, key: str, **_): + invite = db.session.query( + TeamInvite + ).where( + TeamInvite.team_id == team_id + ).where( + TeamInvite.key == key + ).one_or_none() + + if not invite: + abort(404) + + player_team = db.session.query( + PlayerTeam + ).where( + PlayerTeam.player_id == player.steam_id + ).where( + PlayerTeam.team_id == team_id + ).one_or_none() + + if player_team: + abort(409) + + player_team = PlayerTeam() + player_team.player = player + player_team.team_id = team_id + + db.session.add(player_team) + + if invite.delete_on_use: + db.session.delete(invite) + + db.session.commit() + + return make_response({ }, 204) + +@api_team_invite.delete("/id//invite/") +@spec.validate( + resp=Response( + HTTP_204=None, + HTTP_404=None, + ), + operation_id="revoke_invite" +) +@requires_authentication +def revoke_invite(player: Player, team_id: int, key: str, **_): + player_team = db.session.query( + PlayerTeam + ).where( + PlayerTeam.player_id == player.steam_id + ).where( + PlayerTeam.team_id == team_id + ).one_or_none() + + if not player_team: + abort(404) + + invite = db.session.query( + TeamInvite + ).where( + TeamInvite.team_id == team_id + ).where( + TeamInvite.key == key + ).one_or_none() + + if not invite: + abort(404) + + db.session.delete(invite) + db.session.commit() + return make_response({ }, 204) +