diff --git a/requirements.txt b/requirements.txt index 2d58b77..5ecfa58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -sopel>=6.0,<7 +sopel>=7.0,<8 bottle +sqlalchemy diff --git a/sopel_modules/github/formatting.py b/sopel_modules/github/formatting.py index f7e7fa1..98897a3 100644 --- a/sopel_modules/github/formatting.py +++ b/sopel_modules/github/formatting.py @@ -38,37 +38,37 @@ def fmt_url(s, row=None): if not row: row = current_row - return color(s, fg=row[3]) + return color(s, fg=row.url_color) def fmt_tag(s, row=None): if not row: row = current_row - return color(s, fg=row[4]) + return color(s, fg=row.tag_color) def fmt_repo(s, row=None): if not row: row = current_row - return color(s, fg=row[5]) + return color(s, fg=row.repo_color) def fmt_name(s, row=None): if not row: row = current_row - return color(s, fg=row[6]) + return color(s, fg=row.name_color) def fmt_hash(s, row=None): if not row: row = current_row - return color(s, fg=row[7]) + return color(s, fg=row.hash_color) def fmt_branch(s, row=None): if not row: row = current_row - return color(s, fg=row[8]) + return color(s, fg=row.branch_color) def fmt_short_comment_body(body): @@ -270,14 +270,14 @@ def fmt_issue_title_edit(payload=None): def fmt_issue_assignee_message(payload=None): if not payload: payload = current_payload - + target = '' self_assign = False if (payload['assignee']['login'] == payload['sender']['login']): self_assign = True else: target = 'to ' if payload['action'] == 'assigned' else 'from ' - target = target + fmt_name(payload['assignee']['login']) + target = target + fmt_name(payload['assignee']['login']) return '[{}] {} {}{} {} #{} {}'.format( fmt_repo(payload['repository']['name']), fmt_name(payload['sender']['login']), diff --git a/sopel_modules/github/github.py b/sopel_modules/github/github.py index 0fbaa9a..0a4659d 100644 --- a/sopel_modules/github/github.py +++ b/sopel_modules/github/github.py @@ -2,7 +2,7 @@ """ github.py - Sopel GitHub Module Copyright 2015 Max Gurela -Copyright 2019 dgw +Copyright 2019 dgw, Rusty Bower _______ __ __ __ __ | __|__| |_| |--.--.--.| |--. @@ -20,7 +20,11 @@ from . import formatting from .formatting import shorten_url, emojize -from .webhook import setup_webhook, shutdown_webhook +from .utils import get_db_session +from .webhook import setup_webhook, shutdown_webhook, GithubHooks + +from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.orm import scoped_session, sessionmaker import operator from collections import deque @@ -118,6 +122,16 @@ def shutdown(sopel): ''' +def get_db_session(bot): + try: + engine = bot.db.connect() + except OperationalError: + print("OperationalError: Unable to connect to database.") + raise + + return scoped_session(sessionmaker(bind=engine)) + + def fetch_api_endpoint(bot, url): oauth = '' if bot.config.github.client_id and bot.config.github.client_secret: @@ -381,33 +395,45 @@ def configure_repo_messages(bot, trigger): 'state': '{}:{}'.format(repo_name, channel)} auth_url = 'https://github.com/login/oauth/authorize?{}'.format(urlencode(auth_data)) - conn = bot.db.connect() - c = conn.cursor() - - c.execute('SELECT * FROM gh_hooks WHERE channel = ? AND repo_name = ?', (channel, repo_name)) - result = c.fetchone() - if not result: - c.execute('''INSERT INTO gh_hooks (channel, repo_name, enabled) VALUES (?, ?, ?)''', (channel, repo_name, enabled)) - bot.say("Successfully enabled listening for {repo}'s events in {chan}.".format(chan=channel, repo=repo_name)) - bot.say('Great! Please allow me to create my webhook by authorizing via this link: ' + shorten_url(auth_url)) - bot.say('Once that webhook is successfully created, I\'ll post a message in here. Give me about a minute or so to set it up after you authorize. You can configure the colors that I use to display webhooks with {}gh-hook-color'.format(bot.config.core.help_prefix)) - else: - c.execute('''UPDATE gh_hooks SET enabled = ? WHERE channel = ? AND repo_name = ?''', (enabled, channel, repo_name)) - bot.say("Successfully {state} the subscription to {repo}'s events".format(state='enabled' if enabled else 'disabled', repo=repo_name)) - if enabled: + session = get_db_session(bot) + + try: + hook = session.query(GithubHooks) \ + .filter(GithubHooks.channel == channel) \ + .filter(GithubHooks.repo_name == repo_name) \ + .one_or_none() + if hook is None: + hook = GithubHooks(channel=channel, repo_name=repo_name, enabled=enabled) + session.add(hook) + session.commit() + + bot.say("Successfully enabled listening for {repo}'s events in {chan}.".format(chan=channel, repo=repo_name)) bot.say('Great! Please allow me to create my webhook by authorizing via this link: ' + shorten_url(auth_url)) bot.say('Once that webhook is successfully created, I\'ll post a message in here. Give me about a minute or so to set it up after you authorize. You can configure the colors that I use to display webhooks with {}gh-hook-color'.format(bot.config.core.help_prefix)) - conn.commit() - conn.close() + else: + hook.channel = channel + hook.repo_name = repo_name + hook.enabled = enabled + session.commit() + + bot.say("Successfully {state} the subscription to {repo}'s events".format(state='enabled' if enabled else 'disabled', repo=repo_name)) + if enabled: + bot.say('Great! Please allow me to create my webhook by authorizing via this link: ' + shorten_url(auth_url)) + bot.say('Once that webhook is successfully created, I\'ll post a message in here. Give me about a minute or so to set it up after you authorize. You can configure the colors that I use to display webhooks with {}gh-hook-color'.format(bot.config.core.help_prefix)) + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() @commands('gh-hook-color') @require_chanmsg('[GitHub] GitHub hooks can only be configured in a channel') @example('.gh-hook-color maxpowa/Inumuta 13 15 6 6 14 2') def configure_repo_colors(bot, trigger): - ''' + """ .gh-hook-color - Set custom colors for the webhook messages (Uses mIRC color indicies) - ''' + """ allowed = bot.privileges[trigger.sender].get(trigger.nick, 0) >= OP if not allowed and not trigger.admin: return bot.msg(trigger.sender, 'You must be a channel operator to use this command!') @@ -426,26 +452,40 @@ def configure_repo_colors(bot, trigger): if len(colors) != 6: return bot.say('You must provide exactly 6 colors! See "{}help gh-hook-color" for more information.'.format(bot.config.core.help_prefix)) - conn = bot.db.connect() - c = conn.cursor() - - c.execute('SELECT * FROM gh_hooks WHERE channel = ? AND repo_name = ?', (channel, repo_name)) - result = c.fetchone() - if not result: - return bot.say('Please use "{}gh-hook {} enable" before attempting to configure colors!'.format(bot.config.core.help_prefix, repo_name)) - else: - combined = colors - combined.append(channel) - combined.append(repo_name) - c.execute('''UPDATE gh_hooks SET repo_color = ?, name_color = ?, branch_color = ?, tag_color = ?, - hash_color = ?, url_color = ? WHERE channel = ? AND repo_name = ?''', combined) - conn.commit() - c.execute('SELECT * FROM gh_hooks WHERE channel = ? AND repo_name = ?', (channel, repo_name)) - row = c.fetchone() - bot.say("[{}] Example name: {} tag: {} commit: {} branch: {} url: {}".format( - formatting.fmt_repo(repo_name, row), - formatting.fmt_name(trigger.nick, row), - formatting.fmt_tag('tag', row), - formatting.fmt_hash('c0mm17', row), - formatting.fmt_branch('master', row), - formatting.fmt_url('http://git.io/', row))) + session = get_db_session(bot) + + try: + hook = session.query(GithubHooks) \ + .filter(GithubHooks.channel == channel) \ + .filter(GithubHooks.repo_name == repo_name) \ + .one_or_none() + if hook is None: + return bot.say('Please use "{}gh-hook {} enable" before attempting to configure colors!'.format(bot.config.core.help_prefix, repo_name)) + else: + hook.channel = channel + hook.repo_name = repo_name + hook.repo_color = colors[0] + hook.name_color = colors[1] + hook.branch_color = colors[2] + hook.tag_color = colors[3] + hook.hash_color = colors[4] + hook.url_color = colors[5] + session.commit() + + row = session.query(GithubHooks) \ + .filter(GithubHooks.channel == channel) \ + .filter(GithubHooks.repo_name == repo_name) \ + .one_or_none() + + bot.say("[{}] Example name: {} tag: {} commit: {} branch: {} url: {}".format( + formatting.fmt_repo(repo_name, row), + formatting.fmt_name(trigger.nick, row), + formatting.fmt_tag('tag', row), + formatting.fmt_hash('c0mm17', row), + formatting.fmt_branch('master', row), + formatting.fmt_url('https://git.io/', row))) + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() diff --git a/sopel_modules/github/utils.py b/sopel_modules/github/utils.py new file mode 100644 index 0000000..697388c --- /dev/null +++ b/sopel_modules/github/utils.py @@ -0,0 +1,28 @@ +# coding=utf8 +""" +webhook.py - Sopel GitHub Module +Copyright 2015 Max Gurela +Copyright 2019 dgw, Rusty Bower + + _______ __ __ __ __ +| __|__| |_| |--.--.--.| |--. +| | | | _| | | || _ | +|_______|__|____|__|__|_____||_____| + ________ __ __ __ +| | | |.-----.| |--.| |--.-----.-----.| |--.-----. +| | | || -__|| _ || | _ | _ || <|__ --| +|________||_____||_____||__|__|_____|_____||__|__|_____| + +""" +from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.orm import scoped_session, sessionmaker + + +def get_db_session(bot): + try: + engine = bot.db.connect() + except OperationalError: + print("OperationalError: Unable to connect to database.") + raise + + return scoped_session(sessionmaker(bind=engine)) diff --git a/sopel_modules/github/webhook.py b/sopel_modules/github/webhook.py index 2756624..952f2d7 100644 --- a/sopel_modules/github/webhook.py +++ b/sopel_modules/github/webhook.py @@ -2,7 +2,7 @@ """ webhook.py - Sopel GitHub Module Copyright 2015 Max Gurela -Copyright 2019 dgw +Copyright 2019 dgw, Rusty Bower _______ __ __ __ __ | __|__| |_| |--.--.--.| |--. @@ -24,6 +24,12 @@ from .formatting import get_formatted_response from .formatting import fmt_repo from .formatting import fmt_name +from .utils import get_db_session + +from sqlalchemy import Boolean, Column, Integer, String +from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker from threading import Thread import bottle @@ -33,6 +39,22 @@ # Because I'm a horrible person sopel_instance = None +Base = declarative_base() + + +class GithubHooks(Base): + __tablename__ = 'gh_hooks' + channel = Column(String(255), primary_key=True) + repo_name = Column(String(255), primary_key=True) + enabled = Column(Boolean, default=True) + url_color = Column(Integer, default=2) + tag_color = Column(Integer, default=6) + repo_color = Column(Integer, default=13) + name_color = Column(Integer, default=15) + hash_color = Column(Integer, default=14) + branch_color = Column(Integer, default=6) + + def setup_webhook(sopel): global sopel_instance sopel_instance = sopel @@ -46,32 +68,14 @@ def setup_webhook(sopel): sopel.memory['gh_webhook_server'] = base sopel.memory['gh_webhook_thread'] = server - conn = sopel.db.connect() - c = conn.cursor() - + # Catch any errors connecting to database try: - c.execute('SELECT * FROM gh_hooks') - except Exception: - create_table(sopel, c) - conn.commit() - conn.close() - - -def create_table(bot, c): - primary_key = '(channel, repo_name)' - - c.execute('''CREATE TABLE IF NOT EXISTS gh_hooks ( - channel TEXT, - repo_name TEXT, - enabled BOOL DEFAULT 1, - url_color TINYINT DEFAULT 2, - tag_color TINYINT DEFAULT 6, - repo_color TINYINT DEFAULT 13, - name_color TINYINT DEFAULT 15, - hash_color TINYINT DEFAULT 14, - branch_color TINYINT DEFAULT 6, - PRIMARY KEY {0} - )'''.format(primary_key)) + engine = sopel.db.connect() + except OperationalError: + print("OperationalError: Unable to connect to database.") + raise + + Base.metadata.create_all(engine) def shutdown_webhook(sopel): @@ -102,10 +106,35 @@ def stop(self): def get_targets(repo): - conn = sopel_instance.db.connect() - c = conn.cursor() - c.execute('SELECT * FROM gh_hooks WHERE repo_name = ? AND enabled = 1', (repo.lower(), )) - return c.fetchall() + session = get_db_session(sopel_instance) + + try: + targets = session.query(GithubHooks) \ + .filter(GithubHooks.repo_name == repo.lower()) \ + .filter(GithubHooks.enabled == 1) \ + .all() + return targets + except SQLAlchemyError: + session.rollback() + raise + finally: + session.close() + + +def process_payload(payload, targets): + if payload['event'] == 'ping': + for row in targets: + sopel_instance.msg(row.channel, '[{}] {}: {} (Your webhook is now enabled)'.format( + fmt_repo(payload['repository']['name'], row), + fmt_name(payload['sender']['login'], row), + payload['zen'])) + return + + for row in targets: + messages = get_formatted_response(payload, row) + # Write the formatted message(s) to the channel + for message in messages: + sopel_instance.msg(row.channel, message) def process_payload(payload, targets): @@ -144,7 +173,7 @@ def webhook(): payload_handler.start() # send HTTP response ASAP, hopefully within GitHub's very short timeout - return '{"channels":' + json.dumps([target[0] for target in targets]) + '}' + return '{"channels":' + json.dumps([target.channel for target in targets]) + '}' @bottle.get('/auth')