diff --git a/.github/workflows/make-lint.yml b/.github/workflows/make-lint.yml deleted file mode 100644 index c18df29..0000000 --- a/.github/workflows/make-lint.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: lint - -on: - push: - -jobs: - flake8: - runs-on: ubuntu-latest - steps: - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.x - architecture: x64 - - name: Checkout PyTorch - uses: actions/checkout@master - - name: Install flake8 - run: pip install flake8 - - name: Run flake8 - uses: suo/flake8-github-action@releases/v1 - with: - checkName: 'flake8' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/make-test.yml b/.github/workflows/make-test.yml deleted file mode 100644 index 6b407f5..0000000 --- a/.github/workflows/make-test.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: make-test - -on: - push: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - with: - fetch-depth: 1 - - name: Run Makefile - run: make test diff --git a/.github/workflows/release-container.yml b/.github/workflows/release-container.yml index a3064b8..360e7a1 100644 --- a/.github/workflows/release-container.yml +++ b/.github/workflows/release-container.yml @@ -1,23 +1,65 @@ -# Workflow built using instructions from -# https://docs.github.com/en/actions/guides/publishing-docker-images - -name: Create and publish a Docker image +name: Mailblaster CI on: push: branches: - - main + - "main" + tags: + - "*" pull_request: - branches: - - main env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: - build-and-push-image: + test: + name: Pytest the Bot + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + fetch-depth: 1 + - name: Run Makefile + run: make test + + formatblack: + name: Check Black Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Check files using the black formatter + uses: rickstaa/action-black@v1 + id: action_black + with: + black_args: "." + + - name: Annotate diff changes using reviewdog + if: steps.action_black.outputs.is_formatted == 'true' + uses: reviewdog/action-suggester@v1 + with: + tool_name: blackfmt + + - name: Fail if actions taken + if: steps.action_black.outputs.is_formatted == 'true' + run: exit 1 + + - name: Discord notification + if: ${{ failure() }} + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + uses: Ilshidur/action-discord@master + with: + args: "Black formatter reported errors in {{ EVENT_PAYLOAD.pull_request.html_url }} !" + + build-and-publish-image: + # Only publish on tags + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + + name: Build and Publish Registry Image runs-on: ubuntu-latest + permissions: contents: read packages: write diff --git a/email_blaster/cogs/check_email.py b/email_blaster/cogs/check_email.py index 23e3372..e94f8fa 100644 --- a/email_blaster/cogs/check_email.py +++ b/email_blaster/cogs/check_email.py @@ -28,7 +28,7 @@ def cog_unload(self): @tasks.loop(minutes=3) async def check_email(self): - alert_chennel_id = self.bot.config['alertschannel'] + alert_chennel_id = self.bot.config["alertschannel"] alert_chanel = self.bot.get_channel(int(alert_chennel_id)) logging.debug(f"Checking for new emails, sending info to {alert_chennel_id}") @@ -36,7 +36,7 @@ async def check_email(self): email_content = self.get_new_emails() msg_chunk = self.split_into_chunks(email_content, 1900) - logging.debug('Split into chunks') + logging.debug("Split into chunks") logging.debug(msg_chunk) if len(msg_chunk) > 0: @@ -50,18 +50,20 @@ async def check_email(self): await alert_chanel.send(payload) else: - logging.debug('Nothing to send.') + logging.debug("Nothing to send.") @check_email.before_loop async def before_email(self): # Wait till the bot is ready - logging.info('Email Checking Cog is loaded and ready, waiting for bot to start..') + logging.info( + "Email Checking Cog is loaded and ready, waiting for bot to start.." + ) await self.bot.wait_until_ready() # Start the imap connector - self.email_address = self.bot.config['email'] - self.email_password = self.bot.config['emailpassword'] - self.email_server = self.bot.config['emailserver'] + self.email_address = self.bot.config["email"] + self.email_password = self.bot.config["emailpassword"] + self.email_server = self.bot.config["emailserver"] # login to mailserver. self.login() @@ -78,11 +80,11 @@ def get_new_emails(self): # Select mailbox try: - self.mail.select('inbox') + self.mail.select("inbox") except socket.error as e: - logging.error(f'Doing a socket reconnect, got error: {e}') + logging.error(f"Doing a socket reconnect, got error: {e}") self.login() - self.mail.select('inbox') + self.mail.select("inbox") logging.debug("Checking for new emails.") # From here https://humberto.io/blog/sending-and-receiving-emails-with-python/ @@ -95,14 +97,14 @@ def get_new_emails(self): # Fetch the mail using each ID for i in mail_ids: - status, data = self.mail.fetch(i, '(RFC822)') + status, data = self.mail.fetch(i, "(RFC822)") for response_part in data: if isinstance(response_part, tuple): message = email.message_from_bytes(response_part[1]) - mail_from = message['from'] - mail_subject = message['subject'] + mail_from = message["from"] + mail_subject = message["subject"] if message.is_multipart(): - mail_content = '' + mail_content = "" # on multipart we have the text message and # another things like annex, and html version @@ -111,23 +113,23 @@ def get_new_emails(self): for part in message.get_payload(): # if the content type is text/plain # we extract it - if part.get_content_type() == 'text/plain': + if part.get_content_type() == "text/plain": mail_content += part.get_payload() else: # if the message isn't multipart, just extract it mail_content = message.get_payload() # and then let's show its result - logging.info(f'From: {mail_from}') - logging.info(f'Subject: {mail_subject}') - logging.info(f'Content: {mail_content}') + logging.info(f"From: {mail_from}") + logging.info(f"Subject: {mail_subject}") + logging.info(f"Content: {mail_content}") return mail_content return "" # Return empty string otherwise @check_email.error async def exception_catching_callback(self, e): - logging.error(f'caught error: {e}') + logging.error(f"caught error: {e}") quit() def split_into_chunks(self, string, max_len): @@ -135,13 +137,13 @@ def split_into_chunks(self, string, max_len): i = 0 while i < len(string): - if i+max_len < len(string): - chunks.append(string[i:i+max_len]) + if i + max_len < len(string): + chunks.append(string[i : i + max_len]) else: - chunks.append(string[i:len(string)]) + chunks.append(string[i : len(string)]) i += max_len - return(chunks) + return chunks def setup(bot): diff --git a/email_blaster/cogs/info.py b/email_blaster/cogs/info.py index c20f2d7..9435ae6 100644 --- a/email_blaster/cogs/info.py +++ b/email_blaster/cogs/info.py @@ -16,17 +16,19 @@ def __init__(self, bot): async def on_member_join(self, member): channel = member.guild.system_channel if channel is not None: - await channel.send('Welcome {0.mention}.'.format(member)) + await channel.send("Welcome {0.mention}.".format(member)) @commands.Cog.listener() async def on_ready(self): - status_channel = self.bot.get_channel(int('590312300695650305')) + status_channel = self.bot.get_channel(int("590312300695650305")) - await status_channel.send(f'Email Blaster version {self.bot.version} just restarted.') + await status_channel.send( + f"Email Blaster version {self.bot.version} just restarted." + ) @commands.command() async def version(self, ctx, *, member: discord.Member = None): - await ctx.send(f'I am running version {self.bot.version}.') + await ctx.send(f"I am running version {self.bot.version}.") # This wont work till discord 2.0 # @commands.command() diff --git a/email_blaster/keyValueTable.py b/email_blaster/keyValueTable.py index 4426ae4..fbb9a93 100644 --- a/email_blaster/keyValueTable.py +++ b/email_blaster/keyValueTable.py @@ -7,13 +7,16 @@ class KeyValueTable(dict): - ''' + """ A dict-like object inheriting dict and allowing manipulation of a database via dict-like interface. - ''' + """ + def __init__(self, filename=None): self.conn = sqlite3.connect(filename) - self.conn.execute("CREATE TABLE IF NOT EXISTS config (key text unique, value text)") + self.conn.execute( + "CREATE TABLE IF NOT EXISTS config (key text unique, value text)" + ) def commit(self): self.conn.commit() @@ -23,22 +26,22 @@ def close(self): self.conn.close() def __len__(self): - rows = self.conn.execute('SELECT COUNT(*) FROM config').fetchone()[0] + rows = self.conn.execute("SELECT COUNT(*) FROM config").fetchone()[0] return rows if rows is not None else 0 def iterkeys(self): self.conn.cursor() - for row in self.conn.execute('SELECT key FROM config'): + for row in self.conn.execute("SELECT key FROM config"): yield row[0] def itervalues(self): c = self.conn.cursor() - for row in c.execute('SELECT value FROM config'): + for row in c.execute("SELECT value FROM config"): yield row[0] def iteritems(self): c = self.conn.cursor() - for row in c.execute('SELECT key, value FROM config'): + for row in c.execute("SELECT key, value FROM config"): yield row[0], row[1] def keys(self): @@ -51,22 +54,26 @@ def items(self): return list(self.iteritems()) def __contains__(self, key): - return self.conn.execute('SELECT 1 FROM config WHERE key = ?', - (key,)).fetchone() is not None + return ( + self.conn.execute("SELECT 1 FROM config WHERE key = ?", (key,)).fetchone() + is not None + ) def __getitem__(self, key): - item = self.conn.execute('SELECT value FROM config WHERE key = ?', (key,)).fetchone() + item = self.conn.execute( + "SELECT value FROM config WHERE key = ?", (key,) + ).fetchone() if item is None: raise KeyError(key) return item[0] def __setitem__(self, key, value): - self.conn.execute('REPLACE INTO config (key, value) VALUES (?,?)', (key, value)) + self.conn.execute("REPLACE INTO config (key, value) VALUES (?,?)", (key, value)) def __delitem__(self, key): if key not in self: raise KeyError(key) - self.conn.execute('DELETE FROM config WHERE key = ?', (key,)) + self.conn.execute("DELETE FROM config WHERE key = ?", (key,)) def __iter__(self): return self.iterkeys() diff --git a/email_blaster/main.py b/email_blaster/main.py index cae32de..0af6298 100644 --- a/email_blaster/main.py +++ b/email_blaster/main.py @@ -15,19 +15,18 @@ class EmailBlaster(object): - def __init__(self): # Create our discord bot - self.bot = commands.Bot(command_prefix='.') + self.bot = commands.Bot(command_prefix=".") # Append our workdir to the path (for importing modules) - self.workdir = '/app/email_blaster/' + self.workdir = "/app/email_blaster/" sys.path.append(self.workdir) # Get the build commit that the code was built with. - self.version = str(os.environ.get('GIT_COMMIT')) # Currently running version + self.version = str(os.environ.get("GIT_COMMIT")) # Currently running version # Find out if we're running in debug mode, or not. - self.debug = str(os.environ.get("DEBUG")).lower() in ('true', '1', 't') + self.debug = str(os.environ.get("DEBUG")).lower() in ("true", "1", "t") # Setup logging. if self.debug: @@ -41,33 +40,37 @@ def __init__(self): self.bot.config = self.get_config() # Package config with bot self.bot.version = self.version # Package version with bot - for filename in os.listdir(self.workdir + 'cogs'): + for filename in os.listdir(self.workdir + "cogs"): logging.info(f"Found file {filename}, loading as extension.") - if filename.endswith('.py'): - self.bot.load_extension(f'cogs.{filename[:-3]}') + if filename.endswith(".py"): + self.bot.load_extension(f"cogs.{filename[:-3]}") def run(self): logging.info(f"using version {self.version}") # Run the discord bot using our token. - self.bot.run(self.bot.config['token']) + self.bot.run(self.bot.config["token"]) def get_config(self): - '''Returns the config or halts loading till a config is found''' + """Returns the config or halts loading till a config is found""" - config_file_location = '/config/config.ini' - database_file_location = '/config/blaster.db' + config_file_location = "/config/config.ini" + database_file_location = "/config/blaster.db" try: - database = self.initalize_database(config_file_location, database_file_location) + database = self.initalize_database( + config_file_location, database_file_location + ) return database except sqlite3.OperationalError: - logging.warn('Bot detected it was running locally! or there was an error finding a db.') - logging.info('Attempting an alternative configuration') + logging.warn( + "Bot detected it was running locally! or there was an error finding a db." + ) + logging.info("Attempting an alternative configuration") - config_file_location = '/tmp/mailblaster/config.ini' - database_file_location = '/tmp/mailblaster/blaster.db' + config_file_location = "/tmp/mailblaster/config.ini" + database_file_location = "/tmp/mailblaster/blaster.db" def initalize_database(self, cfg_file_loc, db_file_loc): # Connects to the blaster database @@ -77,15 +80,17 @@ def initalize_database(self, cfg_file_loc, db_file_loc): # Check if static config.ini exists if os.path.isfile(cfg_file_loc): - logging.info(f'File found at {cfg_file_loc}, attempting to load') + logging.info(f"File found at {cfg_file_loc}, attempting to load") config.read(cfg_file_loc) else: try: - logging.warning('Config file not found! Copying default in.') - copyfile('/app/resources/config.ini', cfg_file_loc) + logging.warning("Config file not found! Copying default in.") + copyfile("/app/resources/config.ini", cfg_file_loc) except PermissionError: - logging.error('Unable to copy file! Permission error! This is not fixed yet!') + logging.error( + "Unable to copy file! Permission error! This is not fixed yet!" + ) _config = {} # Convert the static config to a dict without sections @@ -96,37 +101,43 @@ def initalize_database(self, cfg_file_loc, db_file_loc): _config[key] = value # Funky stuff config = _config - logging.info('Converted config.ini to a dict.') + logging.info("Converted config.ini to a dict.") # Once the config is loaded, and the db we can compare them # Compare try: - assert database['token'] == config['token'] + assert database["token"] == config["token"] except AssertionError: # Assertion error if what we asserted is not true. - logging.info('Static database and configuration database differ! Updating database.') + logging.info( + "Static database and configuration database differ! Updating database." + ) # Mirror the config over. for key in config: database[key] = config[key] - logging.info('Converted ini to database, continuing to load the bot.') + logging.info("Converted ini to database, continuing to load the bot.") database.commit() return database except KeyError: # Key error if token straight up does not exist - logging.warning('Database was detected to be empty!" \ - "Copying in defaults from config.ini.') + logging.warning( + 'Database was detected to be empty!" \ + "Copying in defaults from config.ini.' + ) # Mirror the config over. for key in config: database[key] = config[key] - logging.warning('Default database has been coppied." \ - "Its possible only default values are set, check config.ini.') + logging.warning( + 'Default database has been coppied." \ + "Its possible only default values are set, check config.ini.' + ) database.commit() return database - logging.info('Database and config loaded and up to date!') + logging.info("Database and config loaded and up to date!") database.commit() return database diff --git a/setup.py b/setup.py index 05b44ae..66d2ac3 100644 --- a/setup.py +++ b/setup.py @@ -8,29 +8,28 @@ from setuptools import setup, find_packages -readme = open('README.md').read() +readme = open("README.md").read() setup( - name='email-blaster', - description='A simple tool to forward and email to discord as a blast!', - author='1721 Tidal Force', - author_email='concordroboticssteam@gmail.com', - url='https://github.com/FRC-1721/MailBlaster', - packages=find_packages(include=['mail_blaster']), - package_dir={'mail-blaster': 'mail_blaster'}, + name="email-blaster", + description="A simple tool to forward and email to discord as a blast!", + author="1721 Tidal Force", + author_email="concordroboticssteam@gmail.com", + url="https://github.com/FRC-1721/MailBlaster", + packages=find_packages(include=["mail_blaster"]), + package_dir={"mail-blaster": "mail_blaster"}, entry_points={ - 'console_scripts': [ - 'mail-blaster=mail_blaster.__main__:main', + "console_scripts": [ + "mail-blaster=mail_blaster.__main__:main", ], }, - - python_requires='>=3.6.0', - version='0.0.0', + python_requires=">=3.6.0", + version="0.0.0", long_description=readme, include_package_data=True, install_requires=[ - 'schedule', - 'discord.py', + "schedule", + "discord.py", ], - license='MIT', + license="MIT", ) diff --git a/tests/test_application.py b/tests/test_application.py index 33bd962..9f5474a 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -12,26 +12,25 @@ def app(): @pytest.fixture def kvt(): - return KeyValueTable('/tmp/test_table.db') + return KeyValueTable("/tmp/test_table.db") class TestApplication(object): - def test_versioning(self, app): # App version must be present assert len(app.version) > 1 def test_config_mountpoint(self, app): # Mountpoint must point where we expect - assert os.path.isdir('/config') is True + assert os.path.isdir("/config") is True def test_key_value_table(self, kvt): - kvt['bot_token'] = '0101u72636462615517' - kvt['email_listen'] = 'doooooooooooooot' - kvt['email_password'] = 'password' + kvt["bot_token"] = "0101u72636462615517" + kvt["email_listen"] = "doooooooooooooot" + kvt["email_password"] = "password" assert len(kvt.items()) == 3 - assert kvt['bot_token'] == '0101u72636462615517' + assert kvt["bot_token"] == "0101u72636462615517" kvt.close()