diff --git a/README.md b/README.md index 900bf25..4568c7b 100644 --- a/README.md +++ b/README.md @@ -17,34 +17,36 @@ Allows to buy something from another user. It performs a transfer, between the c ### List last transactions `/movimentos ` List transactions. If the user has a team, list the last `qty` transactions of his team. If the current user doesn't have a team, an error message appears stating how to join a team. - +### List all teams +`/ver-equipas` +List all teams. Provides the team name and team id of each team participating. +### List all teams registered +`/ver-equipas-registo` +List all registered teams. Provides the team name and team id and entry code of each team registered. +### View team details +`/detalhes-equipa ` +Used to list all details of a team. The `team-id` must be provided. +### View user details +`/detalhes-participante <@user|user-id>` +Used to list details of a participant. The `@user` or `user-id` must be provided. ## Current features: - Request origin verification/validation ## To be added -```./ver-equipas``` - List all teams. \ -Can only be performed by admins. Used to list all teams. - - -```./detalhes-equipa ``` \ -Can only be performed by admins. Used to list all details of a team. The `team-id` must be provided. - - ```./bug ``` \ Can only be performed by admins. Used to change all teams balances. - ```./tornar-admin <@user>``` \ Can only be performed by admins. Used to make `@user` an admin. - ## Features to add - Auto add users to channels - Report logs to channel - Report money receival on buy operation - Permissions system - Error codes +- Single user transactions listing ## Problems found - How to create first admin. diff --git a/src/database.py b/src/database.py index 45ba9a0..00b0f3e 100644 --- a/src/database.py +++ b/src/database.py @@ -602,7 +602,7 @@ def get_last_transactions(slack_user_id, max_quantity): try: db_connection = connect() except exceptions.DatabaseConnectionError as ex: - log.critical("Couldn't get user slack details: {}".format(ex)) + log.critical("Couldn't get last transactions: {}".format(ex)) raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) else: cursor = db_connection.cursor() @@ -657,4 +657,182 @@ def get_last_transactions(slack_user_id, max_quantity): result = [r for r in cursor.fetchall()] cursor.close() db_connection.close() + return result + +def get_teams(): + """ Gets the teams list.""" + try: + db_connection = connect() + except exceptions.DatabaseConnectionError as ex: + log.critical("Couldn't get teams: {}".format(ex)) + raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) + else: + cursor = db_connection.cursor() + + sql_string = """ + SELECT team_id, team_name + FROM teams + """ + try: + cursor.execute(sql_string) + except Exception as ex: + log.error("Couldn't get teams list: {}".format(ex)) + cursor.close() + db_connection.close() + raise exceptions.QueryDatabaseError("Could not perform database select query: {}".format(ex)) + else: + result = [r for r in cursor.fetchall()] + cursor.close() + db_connection.close() + return result + +def get_teams_registration(): + """ Gets the registration teams list.""" + try: + db_connection = connect() + except exceptions.DatabaseConnectionError as ex: + log.critical("Couldn't get registration teams: {}".format(ex)) + raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) + else: + cursor = db_connection.cursor() + + sql_string = """ + SELECT team_id, team_name, entry_code + FROM team_registration + """ + try: + cursor.execute(sql_string) + except Exception as ex: + log.error("Couldn't get teams list: {}".format(ex)) + cursor.close() + db_connection.close() + raise exceptions.QueryDatabaseError("Could not perform database select query: {}".format(ex)) + else: + result = [r for r in cursor.fetchall()] + cursor.close() + db_connection.close() + return result + +def get_team_details(team_id): + """ Gets a team details.""" + try: + db_connection = connect() + except exceptions.DatabaseConnectionError as ex: + log.critical("Couldn't get team details: {}".format(ex)) + raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) + else: + cursor = db_connection.cursor() + + sql_string = """ + SELECT team_id, team_name, balance + FROM teams + WHERE team_id = %s + """ + data = ( + team_id, + ) + try: + cursor.execute(sql_string, data) + except Exception as ex: + log.error("Couldn't get team details: {}".format(ex)) + cursor.close() + db_connection.close() + raise exceptions.QueryDatabaseError("Could not perform database select query: {}".format(ex)) + else: + result = cursor.fetchone() + cursor.close() + db_connection.close() + return result + +def get_team_users(team_id): + """ Gets a team users.""" + try: + db_connection = connect() + except exceptions.DatabaseConnectionError as ex: + log.critical("Couldn't get team users: {}".format(ex)) + raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) + else: + cursor = db_connection.cursor() + + sql_string = """ + SELECT slack_id, slack_name, user_id + FROM users + WHERE team = %s + """ + data = ( + team_id, + ) + try: + cursor.execute(sql_string, data) + except Exception as ex: + log.error("Couldn't get team users: {}".format(ex)) + cursor.close() + db_connection.close() + raise exceptions.QueryDatabaseError("Could not perform database select query: {}".format(ex)) + else: + result = [r for r in cursor.fetchall()] + cursor.close() + db_connection.close() + return result + +def get_user_details_from_slack_id(slack_id): + """ Gets an user details from its slack id.""" + try: + db_connection = connect() + except exceptions.DatabaseConnectionError as ex: + log.critical("Couldn't get user details: {}".format(ex)) + raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) + else: + cursor = db_connection.cursor() + + sql_string = """ + SELECT slack_id, slack_name, user_id, team + FROM users + WHERE slack_id = %s + """ + data = ( + slack_id, + ) + try: + cursor.execute(sql_string, data) + except Exception as ex: + log.error("Couldn't get user details: {}".format(ex)) + cursor.close() + db_connection.close() + raise exceptions.QueryDatabaseError("Could not perform database select query: {}".format(ex)) + else: + result = cursor.fetchone() + cursor.close() + db_connection.close() + return result + +def get_user_details_from_user_id(user_id): + """ Gets an user details from its user id.""" + try: + db_connection = connect() + except exceptions.DatabaseConnectionError as ex: + log.critical("Couldn't get user details: {}".format(ex)) + raise exceptions.QueryDatabaseError("Could not connect to database: {}".format(ex)) + else: + cursor = db_connection.cursor() + + sql_string = """ + SELECT slack_id, slack_name, user_id, team + FROM users + WHERE user_id = %s + """ + data = ( + user_id, + ) + try: + cursor.execute(sql_string, data) + except Exception as ex: + log.error("Couldn't get user details: {}".format(ex)) + cursor.close() + db_connection.close() + raise exceptions.QueryDatabaseError("Could not perform database select query: {}".format(ex)) + else: + result = cursor.fetchone() + cursor.close() + db_connection.close() return result \ No newline at end of file diff --git a/src/definitions.py b/src/definitions.py index 3792d94..a6b2d6b 100644 --- a/src/definitions.py +++ b/src/definitions.py @@ -20,8 +20,12 @@ "CHECK_BALANCE": "/saldo", "BUY": "/compra", "LIST_TRANSACTIONS": "/movimentos", + "LIST_TEAMS": "/ver-equipas", + "LIST_TEAMS_REGISTRATION": "/ver-equipas-registo", + "TEAM_DETAILS": "/detalhes-equipa", + "USER_DETAILS": "/detalhes", } INITIAL_TEAM_BALANCE = 200 -SLACK_REQUEST_TIMESTAMP_MAX_GAP_MINUTES = 1.0 +SLACK_REQUEST_TIMESTAMP_MAX_GAP_MINUTES = 60.0 diff --git a/src/dispatcher.py b/src/dispatcher.py index 6666344..14f203d 100644 --- a/src/dispatcher.py +++ b/src/dispatcher.py @@ -10,6 +10,7 @@ import random import string import re +import uuid common.setup_logger() @@ -39,6 +40,14 @@ def general_dispatcher(): buy_dispatcher(request) elif request["command"] == SLACK_COMMANDS["LIST_TRANSACTIONS"]: list_transactions_dispatcher(request) + elif request["command"] == SLACK_COMMANDS["LIST_TEAMS"]: + list_teams_dispatcher(request) + elif request["command"] == SLACK_COMMANDS["LIST_TEAMS_REGISTRATION"]: + list_teams_registration_dispatcher(request) + elif request["command"] == SLACK_COMMANDS["TEAM_DETAILS"]: + team_details_dispatcher(request) + elif request["command"] == SLACK_COMMANDS["USER_DETAILS"]: + user_details_dispatcher(request) else: log.critical("Invalid request command.") @@ -482,6 +491,147 @@ def list_transactions_dispatcher(request): log.error("Failed to save request log on database.") responder.list_transactions_delayed_reply_success(request, transactions) +def list_teams_dispatcher(request): + """Dispatcher to list teams requests/commands.""" + log.debug("List teams request.") + # TODO: Change response to "No teams" if no teams were found. + try: + log.debug("Getting teams") + teams = database.get_teams() + log.debug(teams) + except exceptions.QueryDatabaseError as ex: + log.critical("List teams search failed: {}".format(ex)) + responder.default_error() + try: + database.save_request_log(request, False, "Could not perform teams list search.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + return + else: + log.debug("Retrieved data.") + try: + database.save_request_log(request, True, "Team list collected.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + responder.list_teams_delayed_reply_success(request, teams) + +def list_teams_registration_dispatcher(request): + """Dispatcher to list teams registrations requests/commands.""" + log.debug("List teams request.") + # TODO: Change response to "No teams" if no teams were found. + try: + log.debug("Getting teams registrations.") + teams = database.get_teams_registration() + log.debug(teams) + except exceptions.QueryDatabaseError as ex: + log.critical("List teams search failed: {}".format(ex)) + responder.default_error() + try: + database.save_request_log(request, False, "Could not perform registration teams list search.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + return + else: + log.debug("Retrieved data.") + try: + database.save_request_log(request, True, "Registration team list collected.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + responder.list_teams_registration_delayed_reply_success(request, teams) + +def team_details_dispatcher(request): + """Dispatcher to team details requests/commands.""" + log.debug("Team details request.") + # Get team_id from args + team_id = request["text"] + if not team_id: + # Bad usage + log.warn("Bad format on command given.") + responder.team_details_delayed_reply_bad_usage(request) + try: + database.save_request_log(request, False, "Not enough arguments.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + return + + try: + log.debug("Getting team details.") + details = database.get_team_details(team_id) + log.debug(details) + users = database.get_team_users(team_id) + log.debug(users) + except exceptions.QueryDatabaseError as ex: + log.critical("Team details/users search failed: {}".format(ex)) + responder.default_error() + try: + database.save_request_log(request, False, "Could not fetch team details/users from the database.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + return + else: + log.debug("Retrieved data.") + try: + database.save_request_log(request, True, "Team details collected.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + responder.team_details_delayed_reply_success(request, details, users) + +def user_details_dispatcher(request): + """Dispatcher to user details requests/commands.""" + log.debug("User details request.") + # Get user from args + args = get_request_args(request["text"]) + if not args or len(args) > 1: + # Bad usage + log.warn("Bad format on command given.") + responder.user_details_delayed_reply_bad_usage(request) + try: + database.save_request_log(request, False, "Not enough arguments.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + return + + user = args[0] + user_id = get_slack_user_id_from_arg(user) + if user_id: + try: + user_info = database.get_user_details_from_slack_id(user_id) + except exceptions.QueryDatabaseError as ex: + log.critical("User details search failed: {}".format(ex)) + try: + database.save_request_log(request, False, "Could not fetch user details from the database.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + responder.default_error() + return + elif check_valid_uuid4(user): + try: + user_info = database.get_user_details_from_user_id(user) + except exceptions.QueryDatabaseError as ex: + log.critical("User details search failed: {}".format(ex)) + try: + database.save_request_log(request, False, "Could not fetch user details from the database.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + responder.default_error() + return + else: + log.debug("Both formats invalid.") + try: + database.save_request_log(request, False, "Invalid user_id/slack name.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + responder.user_details_delayed_reply_bad_usage(request) + return + + log.debug(user_info) + try: + database.save_request_log(request, True, "User details fetched.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log on database.") + + responder.user_details_delayed_reply_success(request, user_info) + def add_request_to_queue(request): """ Add a request to the requests queue.""" try: @@ -531,4 +681,13 @@ def parse_transaction_quantity(amount_str): raise exceptions.IntegerParseError("Failed to convert string to int.") def parse_transaction_description(description_list): - return " ".join(description_list) \ No newline at end of file + return " ".join(description_list) + +def check_valid_uuid4(arg): + try: + uuid.UUID(arg, version=4) + except ValueError: + log.warn("Invalid uuid4 format.") + return False + else: + return True \ No newline at end of file diff --git a/src/handlers.py b/src/handlers.py index dcb8e19..de0a5da 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -155,6 +155,118 @@ def list_transactions(): log.error("Failed to save request log.") return responder.unverified_origin_error() +def list_teams(): + """Handler to list teams request.""" + log.debug("New list teams request.") + request_data = dict(request.POST) + + if check_request_origin(request): + if all_elements_on_request(request_data): + # Procceed with request. + log.debug("Request with correct fields, add to queue.") + if dispatcher.add_request_to_queue(request_data): + # Request was added to queue + return responder.confirm_list_teams_command_reception() + else: + # Request wasn't added to queue + return responder.overloaded_error() + else: + # Inform user of incomplete request. + log.warn("Request with invalid payload was sent.") + return responder.default_error() + else: + # Could not validate user request + log.error("Slack request origin verification failed.") + try: + database.save_request_log(request_data, False, "Unverified origin.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log.") + return responder.unverified_origin_error() + +def list_teams_registration(): + """Handler to list teams registration request.""" + log.debug("New list teams registration request.") + request_data = dict(request.POST) + + if check_request_origin(request): + if all_elements_on_request(request_data): + # Procceed with request. + log.debug("Request with correct fields, add to queue.") + if dispatcher.add_request_to_queue(request_data): + # Request was added to queue + return responder.confirm_list_teams_registration_command_reception() + else: + # Request wasn't added to queue + return responder.overloaded_error() + else: + # Inform user of incomplete request. + log.warn("Request with invalid payload was sent.") + return responder.default_error() + else: + # Could not validate user request + log.error("Slack request origin verification failed.") + try: + database.save_request_log(request_data, False, "Unverified origin.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log.") + return responder.unverified_origin_error() + +def team_details(): + """Handler to list a team details.""" + log.debug("New team details request.") + request_data = dict(request.POST) + + if check_request_origin(request): + if all_elements_on_request(request_data): + # Procceed with request. + log.debug("Request with correct fields, add to queue.") + if dispatcher.add_request_to_queue(request_data): + # Request was added to queue + return responder.confirm_team_details_command_reception() + else: + # Request wasn't added to queue + return responder.overloaded_error() + else: + # Inform user of incomplete request. + log.warn("Request with invalid payload was sent.") + return responder.default_error() + else: + # Could not validate user request + log.error("Slack request origin verification failed.") + try: + database.save_request_log(request_data, False, "Unverified origin.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log.") + return responder.unverified_origin_error() + +def user_details(): + """Handler to list a user details.""" + log.debug("New user details request.") + request_data = dict(request.POST) + + if check_request_origin(request): + if all_elements_on_request(request_data): + # Procceed with request. + log.debug("Request with correct fields, add to queue.") + if dispatcher.add_request_to_queue(request_data): + # Request was added to queue + return responder.confirm_user_details_command_reception() + else: + # Request wasn't added to queue + return responder.overloaded_error() + else: + # Inform user of incomplete request. + log.warn("Request with invalid payload was sent.") + return responder.default_error() + else: + # Could not validate user request + log.error("Slack request origin verification failed.") + try: + database.save_request_log(request_data, False, "Unverified origin.") + except exceptions.SaveRequestLogError: + log.error("Failed to save request log.") + return responder.unverified_origin_error() + def all_elements_on_request(request_data): """Check if all elements (keys) are present in the request dictionary""" if all(k in request_data for k in SLACK_REQUEST_DATA_KEYS): @@ -182,6 +294,7 @@ def check_request_origin(request): return False else: log.critical("Header 'X-Slack-Request-Timestamp' value is different than handler server. Refusing request.") + log.debug("Header value: {} | Current timestamp: {}".format(request_timestamp, time.time())) return False else: # No header diff --git a/src/responder.py b/src/responder.py index b1306ff..e1a5116 100644 --- a/src/responder.py +++ b/src/responder.py @@ -58,6 +58,38 @@ def confirm_list_transactions_command_reception(): } return json.dumps(response_content, ensure_ascii=False).encode("utf-8") +def confirm_list_teams_command_reception(): + """Immediate response to a list teams command.""" + response.add_header("Content-Type", "application/json") + response_content = { + "text": "Vou tratar de ir buscar as equipas a participar!", + } + return json.dumps(response_content, ensure_ascii=False).encode("utf-8") + +def confirm_list_teams_registration_command_reception(): + """Immediate response to a list teams registration command.""" + response.add_header("Content-Type", "application/json") + response_content = { + "text": "Vou tratar de ir buscar as equipas registadas!", + } + return json.dumps(response_content, ensure_ascii=False).encode("utf-8") + +def confirm_team_details_command_reception(): + """Immediate response to a team details command.""" + response.add_header("Content-Type", "application/json") + response_content = { + "text": "Vou tratar de ir buscar os detalhes dessa equipa!", + } + return json.dumps(response_content, ensure_ascii=False).encode("utf-8") + +def confirm_user_details_command_reception(): + """Immediate response to a user details command.""" + response.add_header("Content-Type", "application/json") + response_content = { + "text": "Vou tratar de ir buscar os detalhes desse utilizador!", + } + return json.dumps(response_content, ensure_ascii=False).encode("utf-8") + def create_team_delayed_reply_missing_arguments(request): """Delayed response to Slack reporting not enough arguments on create team command""" log.debug("Missing arguments on create team request.") @@ -348,6 +380,116 @@ def list_transactions_delayed_reply_success(request, transaction_list): except exceptions.POSTRequestError: log.critical("Failed to send delayed message to Slack.") +def list_teams_delayed_reply_success(request, teams_list): + """Delayed response to Slack reporting the teams list.""" + response_content = { + "text": "Aqui estão as {} equipas a participar:\n".format(len(teams_list)), + } + + for idx, team in enumerate(teams_list): + log.debug(team) + response_content["text"] += "_{}_: *Nome:* {} | *ID:* {}\n".format(idx + 1, team[1], team[0]) + + try: + if send_delayed_response(request['response_url'], response_content): + log.debug("Delayed message sent successfully.") + else: + log.critical("Delayed message not sent.") + except exceptions.POSTRequestError: + log.critical("Failed to send delayed message to Slack.") + +def list_teams_registration_delayed_reply_success(request, teams_list): + """Delayed response to Slack reporting the registration teams list.""" + response_content = { + "text": "Aqui estão as {} equipas registadas:\n".format(len(teams_list)), + } + + for idx, team in enumerate(teams_list): + log.debug(team) + response_content["text"] += "_{}_: *Nome:* {} | *ID:* {} | *Código:* {}\n".format(idx + 1, team[1], team[0], team[2]) + + try: + if send_delayed_response(request['response_url'], response_content): + log.debug("Delayed message sent successfully.") + else: + log.critical("Delayed message not sent.") + except exceptions.POSTRequestError: + log.critical("Failed to send delayed message to Slack.") + +def team_details_delayed_reply_bad_usage(request): + """Delayed response to Slack reporting a bad usage on team details command.""" + response_content = { + "text": "Má utilização do comando! Utilização: `/detalhes-equipa id-equipa`.", + } + try: + if send_delayed_response(request['response_url'], response_content): + log.debug("Delayed message sent successfully.") + else: + log.critical("Delayed message not sent.") + except exceptions.POSTRequestError: + log.critical("Failed to send delayed message to Slack.") + +def team_details_delayed_reply_success(request, details, users): + """Delayed response to Slack reporting the results of team details command.""" + + response_content = { + "text": "", + } + + if len(details): + log.debug("Team exists.") + response_content["text"] += "Aqui estão os detalhes da equipa:\n" + response_content["text"] += "*Nome:* {} | *Saldo:* {:.2f} | *ID:* {}\n".format(details[1], details[2], details[0]) + if len(users): + log.debug("Team has users.") + for user in users: + response_content["text"] += "_Elemento:_ *Nome:* <@{}|{}> | *ID:* {}\n".format(user[0], user[1], user[2]) + else: + log.debug("Team has no users") + response_content["text"] += "Não foi encontrado nenhum jogador na equipa." + else: + log.debug("Team doesn't exist.") + response_content["text"] += "Não foi encontrada nenhuma equipa com esse ID." + try: + if send_delayed_response(request['response_url'], response_content): + log.debug("Delayed message sent successfully.") + else: + log.critical("Delayed message not sent.") + except exceptions.POSTRequestError: + log.critical("Failed to send delayed message to Slack.") + +def user_details_delayed_reply_bad_usage(request): + """Delayed response to Slack reporting a bad usage on user details command.""" + response_content = { + "text": "Má utilização do comando! Utilização: `/detalhes [@user|user-id]`.\n Podes fornecer tanto o user pelo seu ID, bem como pela @mention.", + } + try: + if send_delayed_response(request['response_url'], response_content): + log.debug("Delayed message sent successfully.") + else: + log.critical("Delayed message not sent.") + except exceptions.POSTRequestError: + log.critical("Failed to send delayed message to Slack.") + +def user_details_delayed_reply_success(request, user_info): + """Delayed response to Slack reporting the results of user details command.""" + + if user_info: + response_content = { + "text": "*Informação:*\n*Nome:* <@{}|{}> | *ID:* {} | *Equipa:* {}".format(user_info[0], user_info[1], user_info[2], user_info[3]), + } + else: + response_content = { + "text": "*Informação:* Não foi encontrado nenhum utilizador com esse ID/nome.", + } + try: + if send_delayed_response(request['response_url'], response_content): + log.debug("Delayed message sent successfully.") + else: + log.critical("Delayed message not sent.") + except exceptions.POSTRequestError: + log.critical("Failed to send delayed message to Slack.") + def default_error(): """Immediate default response to report an error.""" response.add_header("Content-Type", "application/json") diff --git a/src/server.py b/src/server.py index 283d90b..5894b1c 100644 --- a/src/server.py +++ b/src/server.py @@ -34,3 +34,7 @@ def define_routing(app): app.route(path="/check-balance", method=["POST"], callback=handlers.check_balance) app.route(path="/buy", method=["POST"], callback=handlers.buy) app.route(path="/list-transactions", method=["POST"], callback=handlers.list_transactions) + app.route(path="/list-teams", method=["POST"], callback=handlers.list_teams) + app.route(path="/list-teams-registration", method=["POST"], callback=handlers.list_teams_registration) + app.route(path="/team-details", method=["POST"], callback=handlers.team_details) + app.route(path="/user-details", method=["POST"], callback=handlers.user_details)