Skip to content

Commit

Permalink
Merge pull request #334 from idoneam/dev
Browse files Browse the repository at this point in the history
Release v2.1.0: Rutherford Piano Room
  • Loading branch information
le-potate authored Sep 17, 2020
2 parents 8475fe6 + d25649f commit 3c64084
Show file tree
Hide file tree
Showing 18 changed files with 459 additions and 174 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
env/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
requirements.txt

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
17 changes: 9 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ dist: xenial
python:
- "3.6"

env:
global:
- POETRY_VIRTUALENVS_IN_PROJECT=true
services:
- docker

before_install:
- sed -i "s/KEY/$DISCORD_TOKEN/g" config/config.ini
Expand All @@ -17,11 +16,13 @@ install:
- pip install poetry
- poetry install

jobs:
include:
- stage: Build
script: bash ./test.sh
script: poetry run yapf --diff --recursive .
script:
- pip freeze > requirements.txt
- docker build -t canary:latest .
- docker run -d -v $(pwd):/mnt/canary canary:latest > ./container.tmp
- sleep 5
- docker stop $(cat ./container.tmp)
- docker run -v $(pwd):/mnt/canary canary:latest yapf --diff --recursive .

notifications:
email: false
1 change: 1 addition & 0 deletions .yapfignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
env/
14 changes: 12 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
FROM python:3.6-slim-buster

RUN apt update && apt install -y git
# Install base apt dependencies
RUN apt-get update && apt-get install -y git

# Install auxiliary dependencies (for GL, Tex, etc.)
RUN apt-get install -y \
libgl1-mesa-glx \
texlive-latex-extra \
texlive-lang-greek \
dvipng

# Install Poetry (Python dependency manager)
RUN pip install poetry

# Configure Git settings for update command
Expand All @@ -12,4 +22,4 @@ COPY requirements.txt /mnt/
RUN pip3 install -r /mnt/requirements.txt

WORKDIR /mnt/canary
CMD python Main.py
CMD ["python", "Main.py"]
34 changes: 20 additions & 14 deletions Main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#! /usr/bin/env python3
#
# Copyright (C) idoneam (2016-2019)
# Copyright (C) idoneam (2016-2020)
#
# This file is part of Canary
#
Expand Down Expand Up @@ -62,10 +61,14 @@

@bot.event
async def on_ready():
sys.stdout.write(
'Bot is ready, program output will be written to a log file.\n')
if bot.config.dev_log_webhook_id and bot.config.dev_log_webhook_token:
webhook_string = " and to the log webhook"
else:
webhook_string = ""
sys.stdout.write(f'Bot is ready, program output will be written to a '
f'log file{webhook_string}.\n')
sys.stdout.flush()
bot.logger.info('Logged in as {} ({})'.format(bot.user.name, bot.user.id))
bot.dev_logger.info(f'Logged in as {bot.user.name} ({bot.user.id})')


@bot.command()
Expand Down Expand Up @@ -104,7 +107,7 @@ async def restart(ctx):
"""
Restart the bot
"""
bot.logger.info('Bot restart')
bot.dev_logger.info('Bot restart')
await ctx.send('https://streamable.com/dli1')
python = sys.executable
os.execl(python, python, *sys.argv)
Expand All @@ -116,7 +119,7 @@ async def sleep(ctx):
"""
Shut down the bot
"""
bot.logger.info('Received sleep command. Shutting down bot')
bot.dev_logger.info('Received sleep command. Shutting down bot')
await ctx.send('Bye')
await bot.logout()

Expand All @@ -127,7 +130,7 @@ async def update(ctx):
"""
Update the bot by pulling changes from the git repository
"""
bot.logger.info('Update Git repository')
bot.dev_logger.info('Update Git repository')
shell_output = subprocess.check_output("git pull {}".format(
bot.config.repository),
shell=True)
Expand Down Expand Up @@ -160,15 +163,18 @@ async def backup(ctx):
await ctx.send(content='Here you go',
file=discord.File(fp=bot.config.db_path,
filename=backup_filename))
bot.logger.info('Database backup')
bot.dev_logger.info('Database backup')


if __name__ == "__main__":
def main():
for extension in startup:
try:
bot.load_extension(extension)
except Exception as e:
bot.logger.warning('Failed to load extension {}\n{}: {}'.format(
extension,
type(e).__name__, e))
bot.dev_logger.warning(f'Failed to load extension {extension}\n'
f'{type(e).__name__}: {e}')
bot.run(bot.config.discord_key)


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions Martlet.schema
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ CREATE TABLE IF NOT EXISTS `BankTransactions` (

FOREIGN KEY(`UserID`) REFERENCES `Members`(`ID`)
);

CREATE TABLE IF NOT EXISTS `PreviousRoles` (
`ID` INTEGER UNIQUE,
`Roles` TEXT
);
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ You must set certain values in the `config.ini` file, in particular your Discor
* `Repository`: The HTTPS remote for this repository, used by the `update` command as the remote when pulling.
* `[Logging]`
* `LogLevel`: [See this for a list of levels](https://docs.python.org/3/library/logging.html#levels). Logs from exceptions and commands like `mix` and `bac` are at the `info` level. Logging messages from the level selected *and* from more severe levels will be sent to your logging file. For example, setting the level to `info` also sends logs from `warning`, `error` and `critical`, but not from `debug`.
* `LogFile`: The file where the logging output will be sent (will be created there by the bot if it doesn't exist).
* `LogFile`: The file where the logging output will be sent (will be created there by the bot if it doesn't exist). Note that all logs are sent there, including those destined for devs and those destined for mods.
* `DevLogWebhookID`: Optional. If the ID of a webhook is input (and it's token below), logs destined for devs will also be sent to it. These values are contained in the discord webhook url: [discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN](discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN)
* `DevLogWebhookToken`: Optional. See above.
* `ModLogWebhookID`: Optional. If the ID of a webhook is input (and it's token below), logs destined for mods will also be sent to it. See the URL above to see how to find those values.
* `ModLogWebhookToken`: Optional. See above.
* `[DB]`
* `Schema`: Location of the Schema file that creates tables in the database (This file already exists so you shouldn't have to change this unless you rename it or change its location).
* `Path`: Your database file path (will be created there by the bot if it doesn't exist).
Expand Down
83 changes: 71 additions & 12 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,102 @@
import sqlite3
import traceback

# For log webhook
import requests
from discord import Webhook, RequestsWebhookAdapter

__all__ = ['bot', 'developer_role', 'moderator_role']

_parser = parser.Parser()
command_prefix = _parser.command_prefix

# Create parent logger, which will send all logs from the "sub-loggers"
# to the specified log file
_logger = logging.getLogger('Canary')
_logger.setLevel(_parser.log_level)
_handler = logging.FileHandler(filename=_parser.log_file,
encoding='utf-8',
mode='a')
_handler.setFormatter(
_file_handler = logging.FileHandler(filename=_parser.log_file,
encoding='utf-8',
mode='a')
_file_handler.setFormatter(
logging.Formatter('[%(levelname)s] %(asctime)s: %(message)s'))
_logger.addHandler(_handler)
_logger.addHandler(_file_handler)

# Create dev (sub-)logger, which is where errors and messages are logged
# If a dev webhook is specified, logs sent to the dev logger will be
# sent to the webhook
_dev_logger = logging.getLogger('Canary.Dev')
_dev_logger.setLevel(_parser.log_level)

# Create mod (sub-)logger, where info for mods will be logged
# If a mod webhook is specified, logs sent to the mod logger will be
# sent to the webhook. This is always set to the INFO level, since this is
# where info for mods is logged
_mod_logger = logging.getLogger('Canary.Mod')
_mod_logger.setLevel(logging.INFO)


class _WebhookHandler(logging.Handler):
def __init__(self, webhook_id, webhook_token, username=None):
if not username:
self.username = "Bot Logs"
else:
self.username = username
logging.Handler.__init__(self)
self.webhook = Webhook.partial(webhook_id,
webhook_token,
adapter=RequestsWebhookAdapter())

def emit(self, record):
msg = self.format(record)
self.webhook.send(f"```\n{msg}```", username=self.username)


if _parser.dev_log_webhook_id and _parser.dev_log_webhook_token:
_dev_webhook_username = f"{_parser.bot_name} Dev Logs"
_dev_webhook_handler = _WebhookHandler(_parser.dev_log_webhook_id,
_parser.dev_log_webhook_token,
username=_dev_webhook_username)
_dev_webhook_handler.setFormatter(
logging.Formatter('[%(levelname)s] %(asctime)s:\n%(message)s'))
_dev_logger.addHandler(_dev_webhook_handler)

if _parser.mod_log_webhook_id and _parser.mod_log_webhook_token:
_mod_webhook_username = f"{_parser.bot_name} Mod Logs"
_mod_webhook_handler = _WebhookHandler(_parser.mod_log_webhook_id,
_parser.mod_log_webhook_token,
username=_mod_webhook_username)
_mod_webhook_handler.setFormatter(
logging.Formatter('[%(levelname)s] %(asctime)s:\n%(message)s'))
_mod_logger.addHandler(_mod_webhook_handler)


class Canary(commands.Bot):
def __init__(self, *args, **kwargs):
super().__init__(command_prefix, *args, **kwargs)
self.logger = _logger
self.dev_logger = _dev_logger
self.mod_logger = _mod_logger
self.config = _parser
self._start_database()

def _start_database(self):
if not self.config.db_path:
self.logger.warning('No path to database configuration file')
self.dev_logger.warning('No path to database configuration file')
return

self.logger.debug('Initializing SQLite database')
self.dev_logger.debug('Initializing SQLite database')
conn = sqlite3.connect(self.config.db_path)
c = conn.cursor()
with open(self.config.db_schema_path) as fp:
c.executescript(fp.read())
conn.commit()
conn.close()
self.logger.debug('Database is ready')
self.dev_logger.debug('Database is ready')

def log_traceback(self, exception):
self.dev_logger.error("".join(
traceback.format_exception(type(exception), exception,
exception.__traceback__)))

async def on_command_error(self, ctx, error):
"""The event triggered when an error is raised while invoking a command.
Expand Down Expand Up @@ -91,11 +152,9 @@ async def on_command_error(self, ctx, error):
return await ctx.send(
'I could not find that member. Please try again.')

self.logger.error('Ignoring exception in command {}:'.format(
self.dev_logger.error('Ignoring exception in command {}:'.format(
ctx.command))
self.logger.error(''.join(
traceback.format_exception(type(error), error,
error.__traceback__)))
self.log_traceback(error)


# predefined variables to be imported
Expand Down
87 changes: 87 additions & 0 deletions cogs/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import random
from .utils.paginator import Pages
from .utils.requests import fetch
import sqlite3

MCGILL_EXAM_URL = "https://www.mcgill.ca/exams/dates"

Expand Down Expand Up @@ -563,6 +564,92 @@ async def colour(self, ctx, *, arg: str):
fn = "{}.{}".format(match.group(1), ext)
await ctx.send(file=discord.File(fp=buffer, filename=fn))

@commands.Cog.listener()
async def on_member_remove(self, member):
roles_id = [
role.id for role in member.roles if role.name != "@everyone"
]
if roles_id:
conn = sqlite3.connect(self.bot.config.db_path)
c = conn.cursor()
# store roles as a string of IDs separated by spaces
t = (member.id, ' '.join(str(e) for e in roles_id))
c.execute('REPLACE INTO PreviousRoles VALUES (?, ?)', t)
conn.commit()
conn.close()

@commands.command(aliases=['previousroles', 'giverolesback', 'rolesback'])
async def previous_roles(self, ctx, user: discord.Member):
"""Show the list of roles that a user had before leaving, if possible.
A moderator can click the OK react on the message to give these roles back
"""
conn = sqlite3.connect(self.bot.config.db_path)
c = conn.cursor()
fetched_roles = c.execute(
'SELECT Roles FROM PreviousRoles WHERE ID = ?',
(user.id, )).fetchone()
# the above returns a tuple with a string of IDs separated by spaces
if fetched_roles is not None:
roles_id = fetched_roles[0].split(" ")
valid_roles = []
for role_id in roles_id:
role = self.bot.get_guild(self.bot.config.server_id).get_role(
int(role_id))
if role:
valid_roles.append(role)

roles_name = [
"[{}] {}\n".format(i, role.name)
for i, role in enumerate(valid_roles, 1)
]

embed = discord.Embed(title="Loading...")
message = await ctx.send(embed=embed)

if len(valid_roles) > 20:
await message.add_reaction("◀")
await message.add_reaction("▶")
await message.add_reaction("🆗")

p = Pages(
ctx,
item_list=roles_name,
title="{} had the following roles before leaving.\n"
"A {} can add these roles back by reacting with 🆗".format(
user.display_name, self.bot.config.moderator_role),
msg=message,
display_option=(3, 20),
editable_content=True,
editable_content_emoji="🆗",
return_user_on_edit=True)
ok_user = await p.paginate()

while p.edit_mode:
if discord.utils.get(ok_user.roles,
name=self.bot.config.moderator_role):
await user.add_roles(
*valid_roles,
reason="{} used the previous_roles command".format(
ok_user.name))
embed = discord.Embed(
title="{}'s previous roles were successfully "
"added back by {}".format(user.display_name,
ok_user.display_name))
await message.edit(embed=embed)
await message.clear_reaction("◀")
await message.clear_reaction("▶")
await message.clear_reaction("🆗")
return
else:
ok_user = await p.paginate()

else:
embed = discord.Embed(
title="Could not find any roles for this user")
await ctx.send(embed=embed)

conn.close()


def setup(bot):
bot.add_cog(Helpers(bot))
Loading

0 comments on commit 3c64084

Please sign in to comment.