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) +