diff --git a/.gitignore b/.gitignore index 69fdc87..d1577b8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ venv .env/ # config -config.json +config.yaml discord.log # vscode diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e9969e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Will Russell and Justin Chadwell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index dd3cfd9..758b324 100644 --- a/README.md +++ b/README.md @@ -41,17 +41,17 @@ $ pip3 install -r requirements.txt You will need a token for discord. Follow [this guide](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token) to get one. -Add the token, and URL for the form you want to use to `config.json`. - -```json -{ - "token": "token", - "url": "url", - "start_message": "First message when the command is called", - "end_message": "Last command to show all the questions have be answered", - "embed_title": "The title for polls, such as radios", - "prefix": "prefix for the discord bot" -} +Add the token, and URL for the form you want to use to `config.yaml`. + +```yaml +url: + +discord: + token: + start_message: "Hello there! I'm here to help" + end_message: "Someone will be over to help you shortly!" + embed_title: "Respond with one of the options below" + prefix: "!" ``` ### Run diff --git a/config.json b/config.json deleted file mode 100644 index 5928425..0000000 --- a/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "token": "", - "url": "", - "start_message": "Hello there! I'm here to help", - "end_message": "Someone will be over to help you shortly!", - "embed_title": "Respond with one of the options below", - "prefix": "!" -} \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..8ec70ca --- /dev/null +++ b/config.yaml @@ -0,0 +1,8 @@ +url: + +discord: + token: + start_message: "Hello there! I'm here to help" + end_message: "Someone will be over to help you shortly!" + embed_title: "Respond with one of the options below" + prefix: "!" diff --git a/formbot/fields.py b/formbot/fields.py index 9e10fa0..457ca87 100644 --- a/formbot/fields.py +++ b/formbot/fields.py @@ -1,6 +1,5 @@ import re - class Field: def __init__(self, type, name, display=None, required=False, default=None, validator=None, extra=None): @@ -42,7 +41,6 @@ def __str__(self): return result - def email(data, field): # init static variables if not hasattr(email, 'EMAIL_REGEX'): @@ -54,7 +52,6 @@ def email(data, field): else: raise ValueError('invalid email address') - def checkbox(data, field): # init static variables if not hasattr(checkbox, 'TRUE_VALUES'): @@ -76,7 +73,6 @@ def checkbox(data, field): else: return None - def radio(data, field): data = data.lower() diff --git a/formbot/form.py b/formbot/form.py new file mode 100644 index 0000000..2db0c7b --- /dev/null +++ b/formbot/form.py @@ -0,0 +1,60 @@ +import requests + +class Form: + def __init__(self, session, method, action): + self.session = session + self.method = method.upper() + self.action = action + + self.fields = [] + self.name_lookup = {} + self.id_lookup = {} + + def add_field(self, field, id=None): + if field.name in self.name_lookup: + raise ValueError('cannot have duplicate field names') + else: + self.fields.append(field) + self.name_lookup[field.name] = field + if id: + self.id_lookup[id] = field + + def get_field(self, name=None, id=None): + if name and id: + raise ValueError('cannot get by both name and id') + elif name: + return self.name_lookup[name] + elif id: + return self.id_lookup[id] + else: + raise ValueError('missing search specifier (should be name or id)') + + def fill_field(self, name, value): + if name not in self.name_lookup: + raise KeyError(f'{name} does not appear in form') + + field = self.name_lookup[name] + field.fill(value) + + def submit(self): + # populate values + values = {} + for field in self.fields: + if field.required and field.data is None: + raise KeyError( + f'{field.name} is required and has not been provided') + + if field.data is not None: + values[field.name] = field.data + elif field.type == 'hidden': + values[field.name] = field.data or '' + + # send form + req = requests.Request(self.method, self.action, data=values) + resp = self.session.send(req.prepare()) + + # check for submission errors + if resp.status_code >= 400 and resp.status_code < 500: + raise RuntimeError('invalid request during form submission') + if resp.status_code >= 500 and resp.status_code < 600: + raise RuntimeError('internal server error during form submission') diff --git a/formbot/formbot.py b/formbot/formbot.py index ab62b3f..983b044 100644 --- a/formbot/formbot.py +++ b/formbot/formbot.py @@ -1,25 +1,25 @@ import discord from discord.ext import commands -import json import logging +import requests +import yaml from .scraper import FormScraper -with open("config.json") as file: - config = json.load(file) +with open("config.yaml") as file: + config = yaml.load(file, Loader=yaml.BaseLoader) + +bot = commands.Bot(command_prefix=config['discord']['prefix']) -bot = commands.Bot(command_prefix=config['prefix']) scaper_obj = FormScraper(config['url']) responses = {} questions = {} - def main(): logging.basicConfig(level=logging.INFO) - token = config['token'] + token = config['discord']['token'] bot.run(token) - def get_questions(form): fields = form.fields questions = [] @@ -31,63 +31,62 @@ def get_questions(form): elif field.type == 'radio': items = field.display.split(',') embed = discord.Embed( - title=config['embed_title'], colour=0x348DDD) + title=config['discord']['embed_title'], colour=0x348DDD) for index, item in enumerate(items): option = "Option " + str(index + 1) embed.add_field(name=item, value=option, inline=False) questions.append(embed) return questions - @bot.event async def on_ready(): print("Ready") - @bot.event async def on_message(message): if message.author.bot: return - await bot.process_commands(message) - if message.guild is None: + ctx = await bot.get_context(message) + + if ctx.valid: + print('Command') + await bot.process_commands(message) + elif message.channel.type == discord.ChannelType.private: print("DM channel") author = str(message.author.id) - if author in responses and len(responses[author]['responses']) < \ - len(questions[author]['questions']): + if author in responses and len(responses[author]['responses']) < len(questions[author]['questions']): + print("Message: " + str(message.clean_content)) handle_response(message, author) await mentor_response(message) - print(responses) +def handle_response(response, author): + responses[author]['responses'].append(response.content) + index = len(responses[author]['responses']) - 1 + name = questions[author]['names'][index] + responses[author]['form'].fill_field(name, response.content) + print("Response added to field") async def mentor_response(message): author = str(message.author.id) response_length = len(responses[author]['responses']) + if response_length < len(questions[author]['questions']): if isinstance(questions[author]['questions'][response_length], str): - await message.author.send( - questions[author]['questions'][response_length]) + await message.author.send(questions[author]['questions'][response_length]) else: - await message.author.send( - embed=questions[author]['questions'][response_length]) + await message.author.send(embed=questions[author]['questions'][response_length]) else: - await message.author.send(config['end_message']) + await message.author.send(config['discord']['end_message']) responses[author]['form'].submit() del responses[author] del questions[author] - -def handle_response(response, author): - responses[author]['responses'].append(response.content) - index = len(responses[author]['responses']) - 1 - name = questions[author]['names'][index] - responses[author]['form'].fill_field(name, response.content) - print("Response added to field") - - @bot.command() async def mentor(ctx): - form = scaper_obj.extract() + session = requests.session() + form = scaper_obj.extract(session) author = str(ctx.message.author.id) + responses[author] = { "form": form, "responses": [] @@ -96,7 +95,11 @@ async def mentor(ctx): "questions": get_questions(form), "names": [field.name for field in form.fields if not field.hidden] } + print(questions[author]) - await ctx.author.send(config['start_message']) + await ctx.author.send(config['discord']['start_message']) + await mentor_response(ctx.message) - await ctx.message.delete() + if ctx.message.channel.type != discord.ChannelType.private: + await ctx.message.delete() + print("Mentoring session started with {}".format(ctx.message.author.name)) diff --git a/formbot/scraper.py b/formbot/scraper.py index 110a542..c3cdb41 100644 --- a/formbot/scraper.py +++ b/formbot/scraper.py @@ -3,16 +3,17 @@ from urllib.parse import urljoin from .fields import Field +from .form import Form from . import fields - class FormScraper: def __init__(self, url): self.url = url self.doc = None - def extract(self): - session = requests.session() + def extract(self, session=None): + if session is None: + session = requests.session() response = session.get(self.url) self.doc = BeautifulSoup(response.content, features='html.parser') @@ -27,7 +28,7 @@ def extract(self): for element in self.doc.form.find_all(['input', 'textarea']): # create field - field = self.load_field(element) + field = self._load_field(element) if field is None: continue @@ -41,8 +42,8 @@ def extract(self): return form - def load_field(self, element): - display = self.load_label(element) + def _load_field(self, element): + display = self._load_label(element) if element.name == 'textarea': return Field(type='textarea', @@ -78,7 +79,7 @@ def load_field(self, element): radios = self.doc.find_all( 'input', attrs={'type': 'radio', 'name': name}) - labels = [self.load_label(radio) for radio in radios] + labels = [self._load_label(radio) for radio in radios] display = ','.join(labels) required = any(radio.get('required', False) @@ -100,7 +101,7 @@ def load_field(self, element): raise NotImplementedError('form element not supported') - def load_label(self, element): + def _load_label(self, element): # label in tag if 'id' in element.attrs: label = self.doc.find('label', attrs={'for': element['id']}) @@ -113,65 +114,3 @@ def load_label(self, element): return element.attrs[attr] return '' - - -class Form: - def __init__(self, session, method, action): - self.session = session - self.method = method.upper() - self.action = action - - print(self.action) - - self.fields = [] - self.name_lookup = {} - self.id_lookup = {} - - def add_field(self, field, id=None): - if field.name in self.name_lookup: - raise ValueError('cannot have duplicate field names') - else: - self.fields.append(field) - self.name_lookup[field.name] = field - if id: - self.id_lookup[id] = field - - def get_field(self, name=None, id=None): - if name and id: - raise ValueError('cannot get by both name and id') - elif name: - return self.name_lookup[name] - elif id: - return self.id_lookup[id] - else: - raise ValueError('missing search specifier (should be name or id)') - - def fill_field(self, name, value): - if name not in self.name_lookup: - raise KeyError(f'{name} does not appear in form') - - field = self.name_lookup[name] - field.fill(value) - - def submit(self): - # populate values - values = {} - for field in self.fields: - if field.required and field.data is None: - raise KeyError( - f'{field.name} is required and has not been provided') - - if field.data is not None: - values[field.name] = field.data - elif field.type == 'hidden': - values[field.name] = field.data or '' - - # send form - req = requests.Request(self.method, self.action, data=values) - resp = self.session.send(req.prepare()) - - # check for submission errors - if resp.status_code >= 400 and resp.status_code < 500: - raise RuntimeError('invalid request during form submission') - if resp.status_code >= 500 and resp.status_code < 600: - raise RuntimeError('internal server error during form submission') diff --git a/requirements.txt b/requirements.txt index 2bd245d..745fb24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ discord beautifulsoup4 -requests \ No newline at end of file +requests +pyyaml \ No newline at end of file