diff --git a/Pipfile b/Pipfile index 096fb9b3..3cc318a5 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,8 @@ name = "pypi" aiodns = "*" aiohttp = "<2.3.0,>=2.0.0" websockets = ">=4.0,<5.0" +"beautifulsoup4" = "*" +lxml = "*" [dev-packages] "flake8" = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 4e5214bb..5a010121 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d797e580ddcddc99bf058109ab0306ad584c2902752a3d4076ba713fdc580fb7" + "sha256": "b9b192123ee24bf056723e79ecaaf98633d36ec32fd0175f2823d20e13dc882b" }, "pipfile-spec": 6, "requires": { @@ -53,6 +53,15 @@ ], "version": "==2.0.1" }, + "beautifulsoup4": { + "hashes": [ + "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", + "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", + "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" + ], + "index": "pypi", + "version": "==4.6.0" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -67,6 +76,40 @@ ], "version": "==2.6" }, + "lxml": { + "hashes": [ + "sha256:01c45df6d90497c20aa2a07789a41941f9a1029faa30bf725fc7f6d515b1afe9", + "sha256:0c9fef4f8d444e337df96c54544aeb85b7215b2ed7483bb6c35de97ac99f1bcd", + "sha256:0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b", + "sha256:0e7996e9b46b4d8b4ac1c329a00e2d10edcd8380b95d2a676fccabf4c1dd0512", + "sha256:1858b1933d483ec5727549d3fe166eeb54229fbd6a9d3d7ea26d2c8a28048058", + "sha256:1b164bba1320b14905dcff77da10d5ce9c411ac4acc4fb4ed9a2a4d10fae38c9", + "sha256:1b46f37927fa6cd1f3fe34b54f1a23bd5bea1d905657289e08e1297069a1a597", + "sha256:231047b05907315ae9a9b6925751f9fd2c479cf7b100fff62485a25e382ca0d4", + "sha256:28f0c6652c1b130f1e576b60532f84b19379485eb8da6185c29bd8c9c9bc97bf", + "sha256:34d49d0f72dd82b9530322c48b70ac78cca0911275da741c3b1d2f3603c5f295", + "sha256:3682a17fbf72d56d7e46db2e80ca23850b79c28cfe75dcd9b82f58808f730909", + "sha256:3cf2830b9a6ad7f6e965fa53a768d4d2372a7856f20ffa6ce43d2fe9c0d34b19", + "sha256:5b653c9379ce29ce271fbe1010c5396670f018e78b643e21beefbb3dc6d291de", + "sha256:65a272821d5d8194358d6b46f3ca727fa56a6b63981606eac737c86d27309cdd", + "sha256:691f2cd97cf026c611df1ea5055755eec7f878f2d4f4330dc8686583de6fc5fd", + "sha256:6b6379495d3baacf7ed755ac68547c8dff6ce5d37bf370f0b7678888dc1283f9", + "sha256:75322a531504d4f383264391d89993a42e286da8821ddc5ac315e57305cb84f0", + "sha256:7f457cbda964257f443bac861d3a36732dcba8183149e7818ee2fb7c86901b94", + "sha256:7ff1fc76d8804e0f870c343a72007ff587090c218b0f92d8ee784ac2b6eaf5b9", + "sha256:8523fbde9c2216f3f2b950cb01ebe52e785eaa8a07ffeb456dd3576ca1b4fb9b", + "sha256:8f37627f16e026523fca326f1b5c9a43534862fede6c3e99c2ba6a776d75c1ab", + "sha256:a7182ea298cc3555ea56ffbb0748fe0d5e0d81451e2bc16d7f4645cd01b1ca70", + "sha256:abbd2fb4a5a04c11b5e04eb146659a0cf67bb237dd3d7ca3b9994d3a9f826e55", + "sha256:accc9f6b77bed0a6f267b4fae120f6008a951193d548cdbe9b61fc98a08b1cf8", + "sha256:bd88c8ce0d1504fdfd96a35911dd4f3edfb2e560d7cfdb5a3d09aa571ae5fbae", + "sha256:c557ad647facb3c0027a9d0af58853f905e85a0a2f04dcb73f8e665272fcdc3a", + "sha256:defabb7fbb99f9f7b3e0b24b286a46855caef4776495211b066e9e6592d12b04", + "sha256:e2629cdbcad82b83922a3488937632a4983ecc0fed3e5cfbf430d069382eeb9b" + ], + "index": "pypi", + "version": "==4.2.1" + }, "multidict": { "hashes": [ "sha256:0462372fc74e4c061335118a4a5992b9a618d6c584b028ef03cf3e9b88a960e2", @@ -282,10 +325,10 @@ }, "gitpython": { "hashes": [ - "sha256:ad61bc25deadb535b047684d06f3654c001d9415e1971e51c9c20f5b510076e9", - "sha256:b8367c432de995dc330b5b146c5bfdc0926b8496e100fda6692134e00c0dcdc5" + "sha256:05069e26177c650b3cb945dd543a7ef7ca449f8db5b73038b465105673c1ef61", + "sha256:c47cc31af6e88979c57a33962cbc30a7c25508d74a1b3a19ec5aa7ed64b03129" ], - "version": "==2.1.8" + "version": "==2.1.9" }, "idna": { "hashes": [ diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py index 5f5cd85c..aed5a82f 100644 --- a/bot/cogs/logging.py +++ b/bot/cogs/logging.py @@ -15,6 +15,13 @@ def __init__(self, bot: AutoShardedBot): self.bot = bot async def on_ready(self): + log.info('Signed in as:') + log.info('--------------') + log.info(f'Username: {self.bot.user.name}') + log.info(f'User ID: {self.bot.user.id}') + log.info('--------------') + log.info('Serving Team 17 in Code Jam 1!') + log.info('--------------') log.info("Bot connected!") diff --git a/bot/cogs/snakes.py b/bot/cogs/snakes.py index c9ed8042..525be8fb 100644 --- a/bot/cogs/snakes.py +++ b/bot/cogs/snakes.py @@ -1,11 +1,39 @@ # coding=utf-8 import logging +from difflib import get_close_matches +from random import choice from typing import Any, Dict +from aiohttp import ClientSession + +from bs4 import BeautifulSoup + +import discord from discord.ext.commands import AutoShardedBot, Context, command +from bot.selectors import ( + ALT_IMG_SELECTOR, + DID_YOU_KNOW_SELECTOR, + SCIENTIFIC_NAME_SELECTOR, + SNAKE_IMG_SELECTOR, + SNEK_MAP_SELECTOR +) + log = logging.getLogger(__name__) +PYTHON_INFO = { + 'name': 'Python', + 'scientific-name': 'Pseudo anguis', + 'image-url': 'https://www.python.org/static/community_lopython-logo-master-v3-TM.png', + 'url': 'https://en.wikipedia.org/wiki/Python_(programming_language)', + 'map-url': 'https://ih0.redbubble.net/image.80621508.8flat,800x800,075,t.u1.jpg', + 'description': 'Python is an interpreted high-level programmlanguage ' + 'for general-purpose programming. ' + 'Created by Guido van Rossum and first released1991, ' + 'Python has a design philosophy that emphasizes creadability, ' + 'notably using significant whitespace.' +} + class Snakes: """ @@ -15,6 +43,49 @@ class Snakes: def __init__(self, bot: AutoShardedBot): self.bot = bot + async def on_ready(self): + self.session = ClientSession(loop=self.bot.loop) + self.info_url = 'https://snake-facts.weebly.com/' + log.info('Session created.') + + with open('./snakes.txt', encoding='utf-8') as f: + self.sneks = f.read().split('\n') + for i, snek in enumerate(self.sneks): + self.sneks[i] = snek.replace('\u200b', '').replace('\ufeff', '') + log.info('Snakes loaded.') + + def no_sneks_found(self, name: str) -> discord.Embed: + """Helper function if the snake was not found in the directory.""" + em = discord.Embed( + title='No snake found.', + color=discord.Color.green() + ) + + snakes = get_close_matches(name, self.sneks) + + if snakes: + em.description = 'Did you mean...\n' + em.description += '\n'.join(f'`{x}`' for x in snakes) + else: + snakes = 'https://github.com/SharpBit/code-jam-1/blob/master/snakes.txt' + em.description = f'No close matches found. Click [here]({snakes}) for the list of available snakes.' + + return em + + def format_info(self, data: dict, color=discord.Color.green()) -> discord.Embed: + """Formats the info with the given data.""" + em = discord.Embed( + title=f"{data['name']} ({data['scientific-name']})", + description=data['description'], + color=color, + url=data['url'] + ) + + em.set_image(url=data['image-url']) + em.set_thumbnail(url=data['map-url']) + + return em + async def get_snek(self, name: str = None) -> Dict[str, Any]: """ Go online and fetch information about a snake @@ -28,9 +99,72 @@ async def get_snek(self, name: str = None) -> Dict[str, Any]: :param name: Optional, the name of the snake to get information for - omit for a random snake :return: A dict containing information on a snake """ + if name: + if name not in self.sneks: + return self.no_sneks_found(name) + else: + name = choice(self.sneks) - @command() - async def get(self, ctx: Context, name: str = None): + snake = name.lower().replace(' ', '-').replace("'", '') + url = f'{self.info_url}{snake}.html' + + async with self.session.get(url) as resp: + info = await resp.read() + soup = BeautifulSoup(info, 'lxml') + + for x in range(1, 7): + try: + img = soup.select(SNAKE_IMG_SELECTOR.format(x))[0]['src'] + break + except IndexError: + continue + try: + img = img[1:] + except UnboundLocalError: + img = soup.select(ALT_IMG_SELECTOR)[0]['src'][1:] + + names = soup.find('td', class_='wsite-multicol-col') + sci_name = soup.select(SCIENTIFIC_NAME_SELECTOR)[0].text.strip() + description_tag = soup.find(attrs={'property': {'og:description'}}) + + for x in range(1, 7): + try: + location_map = soup.select(SNEK_MAP_SELECTOR.format(x))[0]['src'] + break + except IndexError: + continue + + info = { + 'name': names.h1.string, + 'scientific-name': sci_name, + 'image-url': f'{self.info_url}{img}', + 'map-url': f'{self.info_url}{location_map[1:]}', + 'description': description_tag['content'], + 'url': url + } + + return info + + async def get_snek_fact(self) -> discord.Embed: + """Helper function to get a snake fact.""" + page = choice(self.sneks).replace(' ', '-').replace("'", '') + url = f'{self.info_url}{page}.html' + + async with self.session.get(url) as resp: + response = await resp.read() + soup = BeautifulSoup(response, 'lxml') + fact = soup.select(DID_YOU_KNOW_SELECTOR)[0].text + + em = discord.Embed( + title='Did you know?', + description=fact[13:], + color=discord.Color.green() + ) + + return em + + @command(aliases=['snakes.get', 'snakes.get()', 'get()']) + async def get(self, ctx: Context, *, name: str = None): """ Go online and fetch information about a snake @@ -40,8 +174,26 @@ async def get(self, ctx: Context, name: str = None): :param ctx: Context object passed from discord.py :param name: Optional, the name of the snake to get information for - omit for a random snake """ + # Sends info about the programming language + if name: + if name.lower() == 'python': + em = self.format_info(PYTHON_INFO, discord.Color.blurple()) + return await ctx.send(embed=em) + data = await self.get_snek(name) + # if the snake is not found + if isinstance(data, discord.Embed): + return await ctx.send(embed=data) + # format the given data + em = self.format_info(data) + await ctx.send(embed=em) - # Any additional commands can be placed here. Be creative, but keep it to a reasonable amount! + @command(aliases=['getsnekfact', 'snekfact()', 'get_snek_fact()']) + async def snekfact(self, ctx: Context): + """ + Gets a randomsnek fact from the "Did you know?" cards + that the website has on the right hand side. + """ + await ctx.send(embed=await self.get_snek_fact()) def setup(bot): diff --git a/bot/selectors.py b/bot/selectors.py new file mode 100644 index 00000000..86e89fea --- /dev/null +++ b/bot/selectors.py @@ -0,0 +1,25 @@ +ALT_IMG_SELECTOR = ( + '#wsite-content > div:nth-of-type(2) > div > div > table > ' + 'tbody > tr > td:nth-of-type(1) > div:nth-of-type(5) > div > a > img' +) + +DID_YOU_KNOW_SELECTOR = ( + '#wsite-content > div:nth-of-type(2) > div > div > table > ' + 'tbody > tr > td:nth-of-type(2) > div:nth-of-type(1)' +) + +SNAKE_IMG_SELECTOR = ( + '#wsite-content > div:nth-of-type(2) > div > div > table > ' + 'tbody > tr > td:nth-of-type(1) > div:nth-of-type(4) > div > div > ' + 'table > tbody > tr > td:nth-of-type({0}) > div > div > a > img' +) + +SCIENTIFIC_NAME_SELECTOR = ( + "#wsite-content > div:nth-of-type(1) > div > div > " + "table > tbody > tr > td:nth-of-type(1) > div:nth-of-type(2)" +) + +SNEK_MAP_SELECTOR = ( + "#wsite-content > div:nth-of-type(2) > div > div > table > " + "tbody > tr > td:nth-of-type(2) > div:nth-of-type({0}) > div > a > img" +) diff --git a/run.py b/run.py index 2ec711fd..dfd980e2 100644 --- a/run.py +++ b/run.py @@ -16,7 +16,7 @@ ">>> ", ">> ", "> ", ">>>", ">>", ">" ), # Order matters (and so do commas) - activity=Game(name="Help: bot.help()"), + activity=Game(name="with snekky sneks"), help_attrs={"aliases": ["help()"]}, formatter=Formatter() ) diff --git a/snakes.txt b/snakes.txt new file mode 100644 index 00000000..286bce33 --- /dev/null +++ b/snakes.txt @@ -0,0 +1,95 @@ +african rock python +​amazon tree boa +ball python +​banded water snake +​brown water snake +​burmese python +blue racer +brown snake +​black rat snake +boa constrictor +black-headed python +​black mamba +​boomslang +​bullsnake +​bushmaster +​blood python +brazilian rainbow boa +blue krait +carpet python +​children's python +corn snake +coachwhip +​​california kingsnake +coastal taipan +​central ranges taipan +copperhead +​cottonmouth +​cape cobra +california red-sided gartersnake +checkered gartersnake +​common krait +death adder +diamondback water snake +​dragon snake +​​​dumeril's boa +eastern diamondback rattlesnake +​eastern indigo snake +eastern garter snake +​eastern hognose +​eastern milksnake +​eastern green mamba +emerald tree boa +​eastern brown snake +​egyptian cobra +​eyelash viper +​european cat snake +fer-de-lance +​fox Snake +green anaconda +​golden lancehead +gaboon viper +green tree python +​gopher snake +​grass snake +​horned viper +​inland taipan +​indian cobra +​king cobra +kenyan sand boa +king brown snake +​milk snake +​massasauga rattlesnake +​mojave rattlesnake +mozambique spitting cobra +monocled cobra +​mangrove snake +​mexican black kingsnake +​mamushi +​northern water snake +​olive python +pine snake +prairie rattlesnake +pigmy rattlesnake +​puff adder​ +queen Snake +rubber boa +​red-bellied snake +rosy boa +​reticulated python +​russel's viper +rinkhals +rough green snake +​ribbon snake +​ringneck snake +red-bellied black snake +​southern black racer +​sidewinder​ +spotted python +​tiger snake +​timber rattlesnake +white-lipped python +​western diamondback rattlesnake +woma python +​western hognose snake +​​yellow anaconda