From 88f751bf620e8240235cf4e4688bd3c950e050cf Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 16:05:51 -0400 Subject: [PATCH 1/9] Formatting, linting, workflows --- .flake8 | 8 ++++ .github/workflows/python-lint.yml | 29 ++++++++++++ .gitignore | 3 ++ bot_actions.py | 28 +++++++----- bot_commands.py | 68 +++++++++++++++++++--------- callbacks.py | 18 ++++---- chat_functions.py | 19 +++----- config.py | 73 ++++++++++++++++++++----------- errors.py | 6 ++- main.py | 26 +++++------ message_responses.py | 7 +-- requirements-dev.txt | 4 ++ storage.py | 16 ++++--- 13 files changed, 202 insertions(+), 103 deletions(-) create mode 100644 .flake8 create mode 100644 .github/workflows/python-lint.yml create mode 100644 requirements-dev.txt diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..82f16c4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +max-line-length = 99 +import-order-style = google +exclude = + env/* + venv/* +accept-encodings = utf-8 +ignore = E203 diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml new file mode 100644 index 0000000..ca0415e --- /dev/null +++ b/.github/workflows/python-lint.yml @@ -0,0 +1,29 @@ +name: Python lint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Lint with black + run: | + black . --check + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --statistics diff --git a/.gitignore b/.gitignore index 9f4d89d..672b71e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,8 @@ volunteer_rooms.csv presenters.csv presenter_rooms.csv +# Data files +store/ + # Log files *.log diff --git a/bot_actions.py b/bot_actions.py index 32db290..e73afb3 100644 --- a/bot_actions.py +++ b/bot_actions.py @@ -1,31 +1,36 @@ -from nio import Api +# coding=utf-8 + from hashlib import sha256 -def valid_token(token, tokens,sender): + +from nio import Api + + +def valid_token(token, tokens, sender): h = sha256() h.update(token.encode("utf-8")) msg = h.hexdigest() if msg in tokens: - if tokens[msg] == 'unused': + if tokens[msg] == "unused": return True, msg elif tokens[msg] == sender: return True, msg return False, "" -async def community_invite(client, group, sender):# + +async def community_invite(client, group, sender): # if not group: return - path = "groups/{}/admin/users/invite/{}".format(group,sender) - data = {"user_id":sender} + path = "groups/{}/admin/users/invite/{}".format(group, sender) + data = {"user_id": sender} query_parameters = {"access_token": client.access_token} path = Api._build_path(path, query_parameters) print(path) - await client.send("PUT", - path, - Api.to_json(data), - headers = {"Content-Type": "application/json"} - ) + await client.send( + "PUT", path, Api.to_json(data), headers={"Content-Type": "application/json"} + ) return + def is_admin(user): user = str(user) print(user) @@ -41,5 +46,6 @@ def is_admin(user): print("no admin.csv") return False + def get_alias(roomid): return roomid diff --git a/bot_commands.py b/bot_commands.py index 6418917..33849c4 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -1,7 +1,10 @@ +# coding=utf-8 + +from bot_actions import community_invite, is_admin, valid_token from chat_functions import send_text_to_room -from bot_actions import valid_token, community_invite, is_admin, get_alias from nio import RoomResolveAliasResponse + class Command(object): def __init__(self, client, store, config, command, room, event): """A command made by a user @@ -27,7 +30,6 @@ def __init__(self, client, store, config, command, room, event): self.event = event self.args = self.command.split()[1:] - async def process(self): """Process the command""" trigger = self.command.lower() @@ -38,7 +40,7 @@ async def process(self): elif trigger.startswith("ticket"): await self._process_request("attendee") elif trigger.startswith("volunteer"): - #await self._volunteer_request() + # await self._volunteer_request() await self._process_request("volunteer") elif trigger.startswith("presenter"): await self._process_request("presenter") @@ -52,7 +54,7 @@ async def process(self): async def _process_request(self, ticket_type): """!h $ticket_type $token""" - if not self.args: + if not self.args: response = "You need to add your token after {}".format(self.command) await send_text_to_room(self.client, self.room.room_id, response) return @@ -60,7 +62,10 @@ async def _process_request(self, ticket_type): print("from: " + self.event.sender) token = str(self.args[0]) if len(token) != 64: - response = "Token must be 64 characters, check your ticket again or if you have trouble, please send an email to helpdesk2020@helpdesk.hope.net" + response = ( + "Token must be 64 characters, check your ticket again or if you " + "have trouble, please send an email to helpdesk2020@helpdesk.hope.net" + ) await send_text_to_room(self.client, self.room.room_id, response) return tokens = self.config.tokens @@ -79,7 +84,7 @@ async def _process_request(self, ticket_type): group = self.config.volunteer_community filename = "volunteers.csv" - valid, h = valid_token(token, tokens,self.event.sender) + valid, h = valid_token(token, tokens, self.event.sender) if valid: response = "Verified ticket. You should now be invited to the {} rooms.".format(ticket_type) await send_text_to_room(self.client, self.room.room_id, response) @@ -87,15 +92,22 @@ async def _process_request(self, ticket_type): for r in rooms: await self.client.room_invite(r, self.event.sender) if tokens[h] == "unused": - await send_text_to_room(self.client, self.room.room_id, "Inviting you to the HOPE community...") + await send_text_to_room( + self.client, + self.room.room_id, + "Inviting you to the HOPE community...", + ) await community_invite(self.client, group, self.event.sender) tokens[h] = self.event.sender - with open(filename, 'w') as f: + with open(filename, "w") as f: for key in tokens.keys(): - f.write("%s,%s\n"%(key,tokens[key])) + f.write("%s,%s\n" % (key, tokens[key])) return else: - response = "This is not a valid token, check your ticket again or email helpdesk2020@helpdesk.hope.net" + response = ( + "This is not a valid token, check your ticket again or " + "email helpdesk2020@helpdesk.hope.net" + ) await send_text_to_room(self.client, self.room.room_id, response) return @@ -104,16 +116,26 @@ async def _volunteer_request(self): await send_text_to_room(self.client, self.room.room_id, response) for r in self.config.volunteer_rooms: await self.client.room_invite(r, self.event.sender) - await send_text_to_room(self.client, self.room.room_id, "Inviting you to the HOPE community") - await community_invite(self.client, self.config.volunteer_community, self.event.sender) + await send_text_to_room( + self.client, self.room.room_id, "Inviting you to the HOPE community" + ) + await community_invite( + self.client, self.config.volunteer_community, self.event.sender + ) return async def _show_help(self): """Show the help text""" if not self.args: - text = ("Hello, I'm the HOPE CoreBot! To be invited to the official conference channels message me with `ticket `. You can see more information (important for presenters) at https://wiki.hope.net/index.php?title=Conference_bot") + text = ( + "Hello, I'm the HOPE CoreBot! To be invited to the official " + "conference channels message me with `ticket `. " + "You can see more information (important for presenters) " + "at https://wiki.hope.net/index.php?title=Conference_bot" + ) await send_text_to_room(self.client, self.room.room_id, text) return + async def _the_planet(self): text = "HACK THE PLANET https://youtu.be/YV78vobCyIo?t=55" await send_text_to_room(self.client, self.room.room_id, text) @@ -121,11 +143,12 @@ async def _the_planet(self): async def _trashing(self): text = """They\'re TRASHING our rights, man! They\'re - TRASHING the flow of data! They\'re TRASHING! - TRASHING! TRASHING! HACK THE PLANET! HACK - THE PLANET!""" + TRASHING the flow of data! They\'re TRASHING! + TRASHING! TRASHING! HACK THE PLANET! HACK + THE PLANET!""" await send_text_to_room(self.client, self.room.room_id, text) return + async def _group(self): await send_text_to_room(self.client, self.room.room_id, "inviting to group") await community_invite(self.client, self.config, self.event.sender) @@ -133,17 +156,22 @@ async def _group(self): async def _notice(self): print("notice") if len(self.args) < 2: - await send_text_to_room(self.client, self.room.room_id, "notice args: ,,,") + await send_text_to_room( + self.client, + self.room.room_id, + "notice args: ,,,", + ) return resp = await self.client.room_resolve_alias(self.args[0]) if not isinstance(resp, RoomResolveAliasResponse): print("bad room alias") return room_id = resp.room_id - msg = "@room " + ' '.join(map(str, self.args[1:])) - print("send {} to {}".format(msg,room_id)) + msg = "@room " + " ".join(map(str, self.args[1:])) + print("send {} to {}".format(msg, room_id)) await send_text_to_room(self.client, room_id, msg) return + async def _invite(self): - #invite user to set of rooms + # invite user to set of rooms return diff --git a/callbacks.py b/callbacks.py index c26ddc4..2e3ceef 100644 --- a/callbacks.py +++ b/callbacks.py @@ -1,18 +1,15 @@ -from chat_functions import ( - send_text_to_room, -) +# coding=utf-8 + +import logging + from bot_commands import Command -from nio import ( - JoinError, -) from message_responses import Message +from nio import JoinError -import logging logger = logging.getLogger(__name__) class Callbacks(object): - def __init__(self, client, store, config): """ Args: @@ -60,7 +57,7 @@ async def message(self, room, event): # treat it as a command if has_command_prefix: # Remove the command prefix - msg = msg[len(self.command_prefix):] + msg = msg[len(self.command_prefix) :] command = Command(self.client, self.store, self.config, msg, room, event) await command.process() @@ -75,7 +72,8 @@ async def invite(self, room, event): if type(result) == JoinError: logger.error( f"Error joining room {room.room_id} (attempt %d): %s", - attempt, result.message, + attempt, + result.message, ) else: break diff --git a/chat_functions.py b/chat_functions.py index fd6ffc5..92e6c8d 100644 --- a/chat_functions.py +++ b/chat_functions.py @@ -1,18 +1,15 @@ +# coding=utf-8 + import logging -from nio import ( - SendRetryError -) + from markdown import markdown +from nio import SendRetryError logger = logging.getLogger(__name__) async def send_text_to_room( - client, - room_id, - message, - notice=True, - markdown_convert=True + client, room_id, message, notice=True, markdown_convert=True ): """Send text to a matrix room @@ -43,11 +40,7 @@ async def send_text_to_room( try: await client.room_send( - room_id, - "m.room.message", - content, - ignore_unverified_devices=True, + room_id, "m.room.message", content, ignore_unverified_devices=True, ) except SendRetryError: logger.exception(f"Unable to send message response to {room_id}") - diff --git a/config.py b/config.py index 42d9150..17b345a 100644 --- a/config.py +++ b/config.py @@ -1,11 +1,14 @@ +# coding=utf-8 + +import csv import logging -import re import os -import yaml -import csv +import re import sys -from typing import List, Any +from typing import Any, List + from errors import ConfigError +import yaml logger = logging.getLogger() @@ -24,34 +27,48 @@ def __init__(self, filepath): self.config = yaml.safe_load(file_stream.read()) # Logging setup - formatter = logging.Formatter('%(asctime)s | %(name)s [%(levelname)s] %(message)s') + formatter = logging.Formatter( + "%(asctime)s | %(name)s [%(levelname)s] %(message)s" + ) log_level = self._get_cfg(["logging", "level"], default="INFO") logger.setLevel(log_level) - file_logging_enabled = self._get_cfg(["logging", "file_logging", "enabled"], default=False) - file_logging_filepath = self._get_cfg(["logging", "file_logging", "filepath"], default="bot.log") + file_logging_enabled = self._get_cfg( + ["logging", "file_logging", "enabled"], default=False + ) + file_logging_filepath = self._get_cfg( + ["logging", "file_logging", "filepath"], default="bot.log" + ) if file_logging_enabled: handler = logging.FileHandler(file_logging_filepath) handler.setFormatter(formatter) logger.addHandler(handler) - console_logging_enabled = self._get_cfg(["logging", "console_logging", "enabled"], default=True) + console_logging_enabled = self._get_cfg( + ["logging", "console_logging", "enabled"], default=True + ) if console_logging_enabled: handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) logger.addHandler(handler) # Storage setup - self.database_filepath = self._get_cfg(["storage", "database_filepath"], required=True) - self.store_filepath = self._get_cfg(["storage", "store_filepath"], required=True) + self.database_filepath = self._get_cfg( + ["storage", "database_filepath"], required=True + ) + self.store_filepath = self._get_cfg( + ["storage", "store_filepath"], required=True + ) # Create the store folder if it doesn't exist if not os.path.isdir(self.store_filepath): if not os.path.exists(self.store_filepath): os.mkdir(self.store_filepath) else: - raise ConfigError(f"storage.store_filepath '{self.store_filepath}' is not a directory") + raise ConfigError( + f"storage.store_filepath '{self.store_filepath}' is not a directory" + ) # Matrix bot account setup self.user_id = self._get_cfg(["matrix", "user_id"], required=True) @@ -60,34 +77,40 @@ def __init__(self, filepath): self.user_password = self._get_cfg(["matrix", "user_password"], required=True) self.device_id = self._get_cfg(["matrix", "device_id"], required=True) - self.device_name = self._get_cfg(["matrix", "device_name"], default="nio-template") + self.device_name = self._get_cfg( + ["matrix", "device_name"], default="nio-template" + ) self.homeserver_url = self._get_cfg(["matrix", "homeserver_url"], required=True) self.command_prefix = self._get_cfg(["command_prefix"], default="!c") + " " - self.rooms_path = self._get_cfg(["rooms_path"],required=True) + self.rooms_path = self._get_cfg(["rooms_path"], required=True) self.tokens_path = self._get_cfg(["tokens_path"], required=True) self.community = self._get_cfg(["community"], required=False) - self.volunteer_community = self._get_cfg(["volunteer_community"], required=False) - self.presenter_community = self._get_cfg(["presenter_community"], required=False) - with open(self.tokens_path, 'rt') as f: + self.volunteer_community = self._get_cfg( + ["volunteer_community"], required=False + ) + self.presenter_community = self._get_cfg( + ["presenter_community"], required=False + ) + with open(self.tokens_path, "rt") as f: reader = csv.reader(f) self.tokens = dict(reader) - with open(self.rooms_path, 'rt') as f: - self.rooms = f.read().splitlines() + with open(self.rooms_path, "rt") as f: + self.rooms = f.read().splitlines() try: f = open("volunteers.csv", "rt") reader = csv.reader(f) self.volunteer_tokens = dict(reader) f.close() except FileNotFoundError: - print('No volunteers.csv') + print("No volunteers.csv") self.volunteer_tokens = [] try: f = open("volunteer_rooms.csv", "rt") self.volunteer_rooms = f.read().splitlines() f.close() except FileNotFoundError: - print('No volunteer_rooms.csv') + print("No volunteer_rooms.csv") self.volunteer_rooms = [] try: f = open("presenters.csv", "rt") @@ -95,20 +118,18 @@ def __init__(self, filepath): self.presenter_tokens = dict(reader) f.close() except FileNotFoundError: - print('No presenters.csv') + print("No presenters.csv") self.presenter_tokens = [] try: f = open("presenter_rooms.csv", "rt") self.presenter_rooms = f.read().splitlines() f.close() except FileNotFoundError: - print('No presenter_rooms.csv') + print("No presenter_rooms.csv") self.presenter_rooms = [] + def _get_cfg( - self, - path: List[str], - default: Any = None, - required: bool = True, + self, path: List[str], default: Any = None, required: bool = True, ) -> Any: """Get a config option from a path and option name, specifying whether it is required. diff --git a/errors.py b/errors.py index ec4284e..e937171 100644 --- a/errors.py +++ b/errors.py @@ -1,8 +1,12 @@ +# coding=utf-8 + + class ConfigError(RuntimeError): """An error encountered during reading the config file Args: msg (str): The message displayed to the user on error """ + def __init__(self, msg): - super(ConfigError, self).__init__("%s" % (msg,)) \ No newline at end of file + super(ConfigError, self).__init__("%s" % (msg,)) diff --git a/main.py b/main.py index 19b3cfa..443744f 100644 --- a/main.py +++ b/main.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 +# coding=utf-8 -import logging import asyncio +import logging import sys from time import sleep + +from aiohttp import ClientConnectionError, ServerDisconnectedError +from callbacks import Callbacks +from config import Config from nio import ( AsyncClient, AsyncClientConfig, - RoomMessageText, InviteMemberEvent, - LoginError, LocalProtocolError, + LoginError, + RoomMessageText, ) -from aiohttp import ( - ServerDisconnectedError, - ClientConnectionError -) -from callbacks import Callbacks -from config import Config from storage import Storage logger = logging.getLogger(__name__) @@ -64,13 +63,12 @@ async def main(): # Try to login with the configured username/password try: login_response = await client.login( - password=config.user_password, - device_name=config.device_name, + password=config.user_password, device_name=config.device_name, ) # Check if login failed if type(login_response) == LoginError: - logger.error(f"Failed to login: %s", login_response.message) + logger.error("Failed to login: %s", login_response.message) return False except LocalProtocolError as e: # There's an edge case here where the user hasn't installed the correct C @@ -78,7 +76,8 @@ async def main(): logger.fatal( "Failed to login. Have you installed the correct dependencies? " "https://github.com/poljar/matrix-nio#installation " - "Error: %s", e + "Error: %s", + e, ) return False @@ -101,4 +100,5 @@ async def main(): # Make sure to close the client connection on disconnect await client.close() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/message_responses.py b/message_responses.py index 744bcb3..dbdd870 100644 --- a/message_responses.py +++ b/message_responses.py @@ -1,11 +1,13 @@ -from chat_functions import send_text_to_room +# coding=utf-8 + import logging +from chat_functions import send_text_to_room + logger = logging.getLogger(__name__) class Message(object): - def __init__(self, client, store, config, message_content, room, event): """Initialize a new Message @@ -38,4 +40,3 @@ async def _hello_world(self): """Say hello""" text = "Hello, world!" await send_text_to_room(self.client, self.room.room_id, text) - diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..d44363e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +black +flake8 +flake8-coding +flake8-import-order diff --git a/storage.py b/storage.py index 65473df..a169b5d 100644 --- a/storage.py +++ b/storage.py @@ -1,6 +1,8 @@ -import sqlite3 -import os.path +# coding=utf-8 + import logging +import os.path +import sqlite3 latest_db_version = 0 @@ -34,10 +36,12 @@ def _initial_setup(self): self.cursor = self.conn.cursor() # Sync token table - self.cursor.execute("CREATE TABLE sync_token (" - "dedupe_id INTEGER PRIMARY KEY, " - "token TEXT NOT NULL" - ")") + self.cursor.execute( + "CREATE TABLE sync_token (" + "dedupe_id INTEGER PRIMARY KEY, " + "token TEXT NOT NULL" + ")" + ) logger.info("Database setup complete") From e86753a0d08ccb654c3f086dd09983611d9fa340 Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 20:11:46 -0400 Subject: [PATCH 2/9] lock while accessing tokens[] --- bot_commands.py | 64 +++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/bot_commands.py b/bot_commands.py index 33849c4..893a326 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -1,9 +1,15 @@ # coding=utf-8 +from asyncio import Lock + from bot_actions import community_invite, is_admin, valid_token from chat_functions import send_text_to_room from nio import RoomResolveAliasResponse +_attendee_token_lock = Lock() +_presenter_token_lock = Lock() +_volunteer_token_lock = Lock() + class Command(object): def __init__(self, client, store, config, command, room, event): @@ -68,48 +74,54 @@ async def _process_request(self, ticket_type): ) await send_text_to_room(self.client, self.room.room_id, response) return + lock = _attendee_token_lock tokens = self.config.tokens rooms = self.config.rooms group = self.config.community filename = "tokens.csv" if ticket_type == "presenter": + lock = _presenter_token_lock tokens = self.config.presenter_tokens rooms = self.config.presenter_rooms group = self.config.presenter_community filename = "presenters.csv" - print("presenter") elif ticket_type == "volunteer": + lock = _volunteer_token_lock tokens = self.config.volunteer_tokens rooms = self.config.volunteer_rooms group = self.config.volunteer_community filename = "volunteers.csv" - valid, h = valid_token(token, tokens, self.event.sender) - if valid: - response = "Verified ticket. You should now be invited to the {} rooms.".format(ticket_type) - await send_text_to_room(self.client, self.room.room_id, response) - print(rooms) - for r in rooms: - await self.client.room_invite(r, self.event.sender) - if tokens[h] == "unused": - await send_text_to_room( - self.client, - self.room.room_id, - "Inviting you to the HOPE community...", + # Make sure other tasks don't interfere with our token[] manipulation or writing + async with lock: + valid, h = valid_token(token, tokens, self.event.sender) + if valid: + response = "Verified ticket. You should now be invited to the {} rooms.".format( + ticket_type ) - await community_invite(self.client, group, self.event.sender) - tokens[h] = self.event.sender - with open(filename, "w") as f: - for key in tokens.keys(): - f.write("%s,%s\n" % (key, tokens[key])) - return - else: - response = ( - "This is not a valid token, check your ticket again or " - "email helpdesk2020@helpdesk.hope.net" - ) - await send_text_to_room(self.client, self.room.room_id, response) - return + await send_text_to_room(self.client, self.room.room_id, response) + print(rooms) + for r in rooms: + await self.client.room_invite(r, self.event.sender) + if tokens[h] == "unused": + await send_text_to_room( + self.client, + self.room.room_id, + "Inviting you to the HOPE community...", + ) + await community_invite(self.client, group, self.event.sender) + tokens[h] = self.event.sender + with open(filename, "w") as f: + for key in tokens.keys(): + f.write("%s,%s\n" % (key, tokens[key])) + return + else: + response = ( + "This is not a valid token, check your ticket again or " + "email helpdesk2020@helpdesk.hope.net" + ) + await send_text_to_room(self.client, self.room.room_id, response) + return async def _volunteer_request(self): response = "Inviting you to the HOPE volunteer rooms..." From 93a10fee212deafb1d2cc2b07e041b5a736419ee Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 20:34:01 -0400 Subject: [PATCH 3/9] use logger for commands --- bot_commands.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/bot_commands.py b/bot_commands.py index 893a326..eb61600 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -1,11 +1,14 @@ # coding=utf-8 from asyncio import Lock +import logging from bot_actions import community_invite, is_admin, valid_token from chat_functions import send_text_to_room from nio import RoomResolveAliasResponse +logger = logging.getLogger(__name__) + _attendee_token_lock = Lock() _presenter_token_lock = Lock() _volunteer_token_lock = Lock() @@ -38,6 +41,7 @@ def __init__(self, client, store, config, command, room, event): async def process(self): """Process the command""" + logging.debug("Got command from %s: %r", self.event.sender, self.command) trigger = self.command.lower() if trigger.startswith("help"): await self._show_help() @@ -64,8 +68,7 @@ async def _process_request(self, ticket_type): response = "You need to add your token after {}".format(self.command) await send_text_to_room(self.client, self.room.room_id, response) return - print("args are:" + " ".join([str(x) for x in self.args])) - print("from: " + self.event.sender) + logging.debug("ticket cmd from %s: %r", self.event.sender, self.args) token = str(self.args[0]) if len(token) != 64: response = ( @@ -100,7 +103,7 @@ async def _process_request(self, ticket_type): ticket_type ) await send_text_to_room(self.client, self.room.room_id, response) - print(rooms) + logging.debug("Inviting %s to %s", self.event.sender, ",".join(rooms)) for r in rooms: await self.client.room_invite(r, self.event.sender) if tokens[h] == "unused": @@ -166,7 +169,13 @@ async def _group(self): await community_invite(self.client, self.config, self.event.sender) async def _notice(self): - print("notice") + msg = "@room " + " ".join(map(str, self.args[1:])) + logging.warning( + "notice used by %s at %s to send: %r", + self.event.sender, + self.room.room_id, + msg, + ) if len(self.args) < 2: await send_text_to_room( self.client, @@ -176,11 +185,12 @@ async def _notice(self): return resp = await self.client.room_resolve_alias(self.args[0]) if not isinstance(resp, RoomResolveAliasResponse): - print("bad room alias") + logging.info("notice: bad room alias %s", self.args[0]) + await send_text_to_room( + self.client, self.room.room_id, "Invalid room alias" + ) return room_id = resp.room_id - msg = "@room " + " ".join(map(str, self.args[1:])) - print("send {} to {}".format(msg, room_id)) await send_text_to_room(self.client, room_id, msg) return From 7005f83a1dbdcaa4cc2b75753458bcfea04582fb Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 20:42:50 -0400 Subject: [PATCH 4/9] spawn command handlers in tasks --- callbacks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/callbacks.py b/callbacks.py index 2e3ceef..ebf3715 100644 --- a/callbacks.py +++ b/callbacks.py @@ -1,5 +1,6 @@ # coding=utf-8 +from asyncio import create_task import logging from bot_commands import Command @@ -60,7 +61,8 @@ async def message(self, room, event): msg = msg[len(self.command_prefix) :] command = Command(self.client, self.store, self.config, msg, room, event) - await command.process() + # Spawn a task and don't wait for it + create_task(command.process()) async def invite(self, room, event): """Callback for when an invite is received. Join the room specified in the invite""" From b1e7b7e2f9fd5566e7c53707f58027451249faaf Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 21:11:59 -0400 Subject: [PATCH 5/9] ux shine --- bot_commands.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/bot_commands.py b/bot_commands.py index eb61600..df080fc 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -65,10 +65,15 @@ async def process(self): async def _process_request(self, ticket_type): """!h $ticket_type $token""" if not self.args: - response = "You need to add your token after {}".format(self.command) + response = ( + "Add the ticket code from your email after the command, like this: \n" + f"`{self.command} a1b2c3d4e5...`" + ) await send_text_to_room(self.client, self.room.room_id, response) return - logging.debug("ticket cmd from %s: %r", self.event.sender, self.args) + logging.debug( + "ticket cmd from %s for %s: %r", self.event.sender, ticket_type, self.args + ) token = str(self.args[0]) if len(token) != 64: response = ( @@ -110,7 +115,7 @@ async def _process_request(self, ticket_type): await send_text_to_room( self.client, self.room.room_id, - "Inviting you to the HOPE community...", + f"Inviting you to the HOPE {ticket_type} community...", ) await community_invite(self.client, group, self.event.sender) tokens[h] = self.event.sender @@ -137,7 +142,6 @@ async def _volunteer_request(self): await community_invite( self.client, self.config.volunteer_community, self.event.sender ) - return async def _show_help(self): """Show the help text""" @@ -145,16 +149,14 @@ async def _show_help(self): text = ( "Hello, I'm the HOPE CoreBot! To be invited to the official " "conference channels message me with `ticket `. " - "You can see more information (important for presenters) " - "at https://wiki.hope.net/index.php?title=Conference_bot" + "You can find more information (important for presenters) on the " + "[conference bot wiki](https://wiki.hope.net/index.php?title=Conference_bot)." ) await send_text_to_room(self.client, self.room.room_id, text) - return async def _the_planet(self): text = "HACK THE PLANET https://youtu.be/YV78vobCyIo?t=55" await send_text_to_room(self.client, self.room.room_id, text) - return async def _trashing(self): text = """They\'re TRASHING our rights, man! They\'re @@ -162,7 +164,6 @@ async def _trashing(self): TRASHING! TRASHING! HACK THE PLANET! HACK THE PLANET!""" await send_text_to_room(self.client, self.room.room_id, text) - return async def _group(self): await send_text_to_room(self.client, self.room.room_id, "inviting to group") @@ -192,8 +193,8 @@ async def _notice(self): return room_id = resp.room_id await send_text_to_room(self.client, room_id, msg) - return + await send_text_to_room(self.client, self.room.room_id, "Sent") async def _invite(self): # invite user to set of rooms - return + pass From adf09647533c33c426025a1626e57db1200071a5 Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 21:43:31 -0400 Subject: [PATCH 6/9] re-invite to community too --- bot_commands.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/bot_commands.py b/bot_commands.py index df080fc..f864f2f 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -104,20 +104,17 @@ async def _process_request(self, ticket_type): async with lock: valid, h = valid_token(token, tokens, self.event.sender) if valid: - response = "Verified ticket. You should now be invited to the {} rooms.".format( - ticket_type + response = ( + "Verified ticket. You should now be invited to the HOPE " + f"{ticket_type} chat rooms and community." ) await send_text_to_room(self.client, self.room.room_id, response) logging.debug("Inviting %s to %s", self.event.sender, ",".join(rooms)) for r in rooms: await self.client.room_invite(r, self.event.sender) + await community_invite(self.client, group, self.event.sender) + if tokens[h] == "unused": - await send_text_to_room( - self.client, - self.room.room_id, - f"Inviting you to the HOPE {ticket_type} community...", - ) - await community_invite(self.client, group, self.event.sender) tokens[h] = self.event.sender with open(filename, "w") as f: for key in tokens.keys(): From 9bbfc07fac40e7a92cb599d37232a159845b1629 Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 21:43:39 -0400 Subject: [PATCH 7/9] more logging --- .gitignore | 7 +------ bot_actions.py | 13 ++++++++----- bot_commands.py | 21 +++++++++++++-------- config.py | 11 +++++++---- sample.config.yaml | 1 + 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 672b71e..fb8ec61 100644 --- a/.gitignore +++ b/.gitignore @@ -17,12 +17,7 @@ __pycache__/ # Config file config.yaml -tokens.csv -rooms.csv -volunteers.csv -volunteer_rooms.csv -presenters.csv -presenter_rooms.csv +*.csv # Data files store/ diff --git a/bot_actions.py b/bot_actions.py index e73afb3..000bd2b 100644 --- a/bot_actions.py +++ b/bot_actions.py @@ -1,9 +1,12 @@ # coding=utf-8 from hashlib import sha256 +import logging from nio import Api +logger = logging.getLogger(__name__) + def valid_token(token, tokens, sender): h = sha256() @@ -14,7 +17,7 @@ def valid_token(token, tokens, sender): return True, msg elif tokens[msg] == sender: return True, msg - return False, "" + return False, msg async def community_invite(client, group, sender): # @@ -24,7 +27,7 @@ async def community_invite(client, group, sender): # data = {"user_id": sender} query_parameters = {"access_token": client.access_token} path = Api._build_path(path, query_parameters) - print(path) + logging.debug("community_invite path: %r", path) await client.send( "PUT", path, Api.to_json(data), headers={"Content-Type": "application/json"} ) @@ -33,17 +36,17 @@ async def community_invite(client, group, sender): # def is_admin(user): user = str(user) - print(user) + logging.debug("is_admin? %s", user) try: f = open("admin.csv", "rt") for nick in f.readlines(): - print(nick) + logging.debug("is_admin line: %s", nick) if user == nick.rstrip(): f.close() return True f.close() except FileNotFoundError: - print("no admin.csv") + logging.error("No admin.csv") return False diff --git a/bot_commands.py b/bot_commands.py index f864f2f..8fbc97d 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -71,9 +71,7 @@ async def _process_request(self, ticket_type): ) await send_text_to_room(self.client, self.room.room_id, response) return - logging.debug( - "ticket cmd from %s for %s: %r", self.event.sender, ticket_type, self.args - ) + logging.debug("ticket cmd from %s for %s", self.event.sender, ticket_type) token = str(self.args[0]) if len(token) != 64: response = ( @@ -121,12 +119,19 @@ async def _process_request(self, ticket_type): f.write("%s,%s\n" % (key, tokens[key])) return else: - response = ( - "This is not a valid token, check your ticket again or " - "email helpdesk2020@helpdesk.hope.net" + logging.info( + "ticket invalid: %s: %s %s (%s)", + self.event.sender, + ticket_type, + token, + tokens.get(h, ""), ) - await send_text_to_room(self.client, self.room.room_id, response) - return + # notify outside lock block + response = ( + "This is not a valid token, check your ticket again or " + "email helpdesk2020@helpdesk.hope.net" + ) + await send_text_to_room(self.client, self.room.room_id, response) async def _volunteer_request(self): response = "Inviting you to the HOPE volunteer rooms..." diff --git a/config.py b/config.py index 17b345a..e6e41f4 100644 --- a/config.py +++ b/config.py @@ -34,6 +34,9 @@ def __init__(self, filepath): log_level = self._get_cfg(["logging", "level"], default="INFO") logger.setLevel(log_level) + peewee_log_level = self._get_cfg(["logging", "peewee_level"], default="INFO") + logging.getLogger("peewee").setLevel(peewee_log_level) + file_logging_enabled = self._get_cfg( ["logging", "file_logging", "enabled"], default=False ) @@ -103,14 +106,14 @@ def __init__(self, filepath): self.volunteer_tokens = dict(reader) f.close() except FileNotFoundError: - print("No volunteers.csv") + logger.error("No volunteers.csv") self.volunteer_tokens = [] try: f = open("volunteer_rooms.csv", "rt") self.volunteer_rooms = f.read().splitlines() f.close() except FileNotFoundError: - print("No volunteer_rooms.csv") + logger.error("No volunteer_rooms.csv") self.volunteer_rooms = [] try: f = open("presenters.csv", "rt") @@ -118,14 +121,14 @@ def __init__(self, filepath): self.presenter_tokens = dict(reader) f.close() except FileNotFoundError: - print("No presenters.csv") + logger.error("No presenters.csv") self.presenter_tokens = [] try: f = open("presenter_rooms.csv", "rt") self.presenter_rooms = f.read().splitlines() f.close() except FileNotFoundError: - print("No presenter_rooms.csv") + logger.error("No presenter_rooms.csv") self.presenter_rooms = [] def _get_cfg( diff --git a/sample.config.yaml b/sample.config.yaml index 3733722..bb65c03 100644 --- a/sample.config.yaml +++ b/sample.config.yaml @@ -31,6 +31,7 @@ logging: # Logging level # Allowed levels are 'INFO', 'WARNING', 'ERROR', 'DEBUG' where DEBUG is most verbose level: INFO + peewee_level: INFO # Configure logging to a file file_logging: # Whether logging to a file is enabled From 899ff5a768d42fef6effb6a98839df98ac7fb655 Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 21:57:39 -0400 Subject: [PATCH 8/9] atomic csv write --- bot_commands.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot_commands.py b/bot_commands.py index 8fbc97d..6a983f0 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -2,6 +2,7 @@ from asyncio import Lock import logging +from os import fsync, rename from bot_actions import community_invite, is_admin, valid_token from chat_functions import send_text_to_room @@ -114,9 +115,14 @@ async def _process_request(self, ticket_type): if tokens[h] == "unused": tokens[h] = self.event.sender - with open(filename, "w") as f: + filename_temp = filename + ".atomic" + with open(filename_temp, "w") as f: for key in tokens.keys(): f.write("%s,%s\n" % (key, tokens[key])) + f.flush() + fsync(f.fileno()) + rename(filename_temp, filename) + return else: logging.info( From c9b99acd19ebbc1f1a3b85d1b7fbcd1525f544ba Mon Sep 17 00:00:00 2001 From: Trevor Bergeron Date: Thu, 23 Jul 2020 22:07:39 -0400 Subject: [PATCH 9/9] use csv writer library --- bot_commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot_commands.py b/bot_commands.py index 6a983f0..7e6df58 100644 --- a/bot_commands.py +++ b/bot_commands.py @@ -1,6 +1,7 @@ # coding=utf-8 from asyncio import Lock +import csv import logging from os import fsync, rename @@ -117,8 +118,8 @@ async def _process_request(self, ticket_type): tokens[h] = self.event.sender filename_temp = filename + ".atomic" with open(filename_temp, "w") as f: - for key in tokens.keys(): - f.write("%s,%s\n" % (key, tokens[key])) + csv_writer = csv.writer(f) + csv_writer.writerows(tokens.items()) f.flush() fsync(f.fileno()) rename(filename_temp, filename)