Skip to content
This repository was archived by the owner on Mar 14, 2021. It is now read-only.

Team 21 #12

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cd2958a
testfile for testing commiting inside my text editor. can be used for…
eivl Mar 23, 2018
ed78916
Naive input handling
TildeBeta Mar 23, 2018
0a8ecf2
get_snake info from wikipedia, get_snek missing exception handeling
eivl Mar 25, 2018
194b420
Squashed commit of the following:
TildeBeta Mar 25, 2018
b41ebe6
Merge pull request #1 from discord-python/master
eivl Mar 25, 2018
e10dfd9
Quick fixes
TildeBeta Mar 25, 2018
5f24356
Merge branch 'master' of github.com:eivl/code-jam-1
TildeBeta Mar 25, 2018
33df4d7
Fix flake8 import order issue
TildeBeta Mar 25, 2018
38dadc2
Make import PEP8 after previous change
TildeBeta Mar 25, 2018
9dd9c2f
exception handeling, pageid->wiki_error & dict -> add error key
eivl Mar 25, 2018
8e49271
Merge branch 'master' of https://github.com/eivl/code-jam-1
eivl Mar 25, 2018
6be2ba0
changed to bot.http_session
eivl Mar 25, 2018
a88e00e
map_list and image_list -> full url support
eivl Mar 25, 2018
83a1019
Various fixes
TildeBeta Mar 25, 2018
128efbf
edited code to pure json return instead of text
eivl Mar 25, 2018
01529d4
Turns out this broke the custom invoking
TildeBeta Mar 25, 2018
280bfa9
Oops
TildeBeta Mar 25, 2018
316b55c
replaced spacees with %20 in snake image URL
eivl Mar 25, 2018
20c555a
added thumbnail list to snake dict with 100px width
eivl Mar 25, 2018
f9cf749
Embed thumbnail
TildeBeta Mar 25, 2018
525adc0
Quick fixes
TildeBeta Mar 25, 2018
f844893
New secret command
TildeBeta Mar 25, 2018
a143ae3
Remove test file
TildeBeta Mar 25, 2018
5e7ed88
Better safe than sorry
TildeBeta Mar 25, 2018
676f1ec
Reuse ClientSession
TildeBeta Mar 25, 2018
f2e5d69
Add clarification
TildeBeta Mar 25, 2018
92bc40a
New command and disambiguate changes
TildeBeta Mar 25, 2018
9373d79
Docstring for new command
TildeBeta Mar 25, 2018
69ab790
banned images from guess
eivl Mar 25, 2018
e579d2a
Merge branch 'master' of https://github.com/eivl/code-jam-1
eivl Mar 25, 2018
bfdc89c
more banned images
eivl Mar 25, 2018
98b8ba7
flake8 error in import statement, commented out
eivl Mar 25, 2018
f741a2b
forcing travis rebuild
eivl Mar 25, 2018
a52208a
newline error flake8 fixed
eivl Mar 25, 2018
3e7f499
commands changed to bot.snakes.COMMAND (from bot.COMMAND)
eivl Mar 25, 2018
cc8624a
README info
eivl Mar 25, 2018
11be7f1
README update
eivl Mar 25, 2018
53973e4
README fix
eivl Mar 25, 2018
ce51e6c
Allow user to only run one instance of command at a time
TildeBeta Mar 25, 2018
812e4c2
Fix import order
TildeBeta Mar 25, 2018
cf32fdb
Document locked decorator
TildeBeta Mar 25, 2018
7e5a33c
Clean up get_snek a bit
TildeBeta Mar 25, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ name = "pypi"
aiodns = "*"
aiohttp = "<2.3.0,>=2.0.0"
websockets = ">=4.0,<5.0"
fuzzywuzzy = "*"
python-levenshtein = "*"
"discord.py" = {git = "https://github.com/Rapptz/discord.py", ref = "rewrite", extras = ["voice"]}

[dev-packages]
"flake8" = "*"
Expand Down
30 changes: 26 additions & 4 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 12 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,23 @@ This is the repository for all code relating to our first code jam, in March 201

**This code jam runs from the 23rd of March to the 25th of March, measured using the UTC timezone.** Make sure you open your pull request by then. Once the deadline is up, stop pushing commits - we will not accept any submissions made after this date.

## How To Participate
## What does it do?
Searches Wikipedia for snake information, but first it **converts** your search to a valid snake result. You get a nice table when you invoke the search command.
**bot.snakes.get("viper")**

First things first - set up your repository. Read [this guide on our site](https://pythondiscord.com/info/jams) for information on how to set yourself up for a code jam.
Remember, only one teammate needs to fork the repository - everyone else should be granted access to that fork as a contributor, so that they can work on it directly.
![Multiple_search_results](https://i.imgur.com/9Lij5Jp.png)

Make sure you have the following things installed:
You can off-course search directly for the scientific name.
**bot.snakes.get("Bothriechis schlegelii")

* Python 3.6 or later (installed with the PATH option enabled if you're on Windows)
* Pip - make sure you can run `pip` in a terminal or command prompt
* Pipenv - you can install this by running `pip install pipenv` in a terminal or command prompt
* Like before, make sure you can run `pipenv` in a terminal or command prompt
![Scientific_search](https://i.imgur.com/M7WdO18.png)

Next up, set up your project with `pipenv`. We've [compiled some documentation](./doc) for you to read over if you get stuck - you can find it in the `doc/` folder,
and you absolutely should read all of it, and it will likely answer some of the questions that you have.
**bot.snakes.get()** returns a random snake from wikipedia

Use `pipenv run run.py` to start your project. You can press `CTRL+C` with the bot window selected to stop it.
There is also a guessing game here

Remember, if you need help, you can always ask on the server!
**bot.snakes.guess()**

## The Task
![guess_the_snake](https://i.imgur.com/JWHrDbk.png)

This month's theme is: **Snakes**.

For this code jam, your task will be to create a Snake cog for a [Discord.py rewrite bot](https://github.com/Rapptz/discord.py/tree/rewrite).
You can find the [documentation for Discord.py rewrite here](https://discordpy.readthedocs.io/en/rewrite/). The best cog commands will be
added to the official Python Discord bot and made available to everyone on the server. The overall best cog will be awarded custom Code Jam
Champion roles, but the best commands from the teams who did not win will also be added to our bot, and any users who write something that
ends up in the bot will be awarded Contributor roles on the server.

We have prepared some Discord.py rewrite boilerplate for you in this repo. Fork the repo and work in the file called **snakes.py**, in **bot/cogs**.

This means you won't have to write the basic bot itself, you'll just have to write the stuff that goes in the cog. For those of you with no
discord.py experience, cogs are like little modules that the bot can load, and contain a class with methods that are hooked up to bot commands
(like **bot.tags.get**). That way, when you type `bot.snakes.get('python')`, it will run the method inside the cog that corresponds to this command.

Your initial task will be to write **get_snek**. This is the minimum requirement for this contest, and everyone must do it. **get_snek** will be a
method that goes online and fetches information about a snake. If you run it without providing an argument, it should fetch information about a
random snake, including the name of the snake, a picture of the snake, and various information about it. Is it venomous? Where can it be found?
What information you choose to get is up to you.

`get_snek()` should also take an optional argument `name`, which should be a string that contains the name of a snake. For example, if you do
`get_snek('cobra')`, it should get information about a cobra. `name` should be case insensitive.

If `get_snek('Python')` is called, the method should instead return information about the programming language, but making sure to return the
same type of information as for all the other snakes. Fill in this information in any way you want, try to have some fun with it.

The information should be returned as a dictionary, so that other methods in the Snake class can call it and make use of it.

Once you have finished `get_snek()`, you should make at least two bot commands. The first command, `get()`, should simply call `get_snek()`
with whatever arguments the user provided, and then make a nice embed that it returns to Discord. For example, if the user in the Discord
channel says `bot.snakes.get('anaconda')`, the bot should post an embed that shows a picture of an anaconda and some information about the
snake.

The second command is entirely up to you. You can choose to use `get_snek` for this command as well, or you can come up with something entirely
different. The only requirement is that it is snake related in some way or other. Here is your chance to be creative. It is these commands that
will win or lose you this code jam. The best original ideas for these commands will probably walk away with the victory.

You are allowed to make as many additional commands as you want, but try to keep it a reasonable amount. The team that writes the most commands is
not automatically going to win. One really excellent command is much better than 10 mediocre ones.

---

Have fun, and don't be afraid to ask for help in the usual places if you need it!
Alas, if you are in a voice channel and type **bot.zen** you are greeted with a little easter-egg
214 changes: 204 additions & 10 deletions bot/cogs/snakes.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
# coding=utf-8
import asyncio
import logging
import random
import re
import textwrap
from typing import Any, Dict

from discord.ext.commands import AutoShardedBot, Context, command
import aiohttp
import async_timeout
import discord
from discord.ext.commands import AutoShardedBot, Context, command, bot_has_permissions

from bot.converters import Snake
from bot.decorators import locked
from bot.utils import disambiguate

log = logging.getLogger(__name__)
URL = "https://en.wikipedia.org/w/api.php?"


class Snakes:
"""
Snake-related commands
"""

# I really hope this works
wiki_sects = re.compile(r'(?:=+ (.*?) =+)(.*?\n\n)', flags=re.DOTALL)
wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL)

valid = ('gif', 'png', 'jpeg', 'jpg', 'webp')

def __init__(self, bot: AutoShardedBot):
self.bot = bot

async def get_snek(self, name: str = None) -> Dict[str, Any]:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove what it returns? That's great documenting!

async def fetch(self, session, url, params=None):
if params is None:
params = {}

async with async_timeout.timeout(10):
async with session.get(url, params=params) as response:
return await response.json()

async def get_snek(self, name: str) -> Dict[str, Any]:
"""
Go online and fetch information about a snake

Expand All @@ -25,23 +51,191 @@ async def get_snek(self, name: str = None) -> Dict[str, Any]:
If "python" is given as the snake name, you should return information about the programming language, but with
all the information you'd provide for a real snake. Try to have some fun with this!

:param name: Optional, the name of the snake to get information for - omit for a random snake
:param name: The name of the snake to get information for - omit for a random snake
:return: A dict containing information on a snake
"""
snake_info = {}

@command()
async def get(self, ctx: Context, name: str = None):
"""
Go online and fetch information about a snake
async with aiohttp.ClientSession() as session:
params = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't have to be defined each time it's called, maybe class property?

'format': 'json',
'action': 'query',
'list': 'search',
'srsearch': name,
'utf8': '',
'srlimit': '1',
}

json = await self.fetch(session, URL, params=params)

# wikipedia does have a error page
try:
pageid = json["query"]["search"][0]["pageid"]
except KeyError:
# Wikipedia error page ID(?)
pageid = 41118

params = {
'format': 'json',
'action': 'query',
'prop': 'extracts|images|info',
'exlimit': 'max',
'explaintext': '',
'inprop': 'url',
'pageids': pageid
}

This should make use of your `get_snek` method, using it to get information about a snake. This information
should be sent back to Discord in an embed.
json = await self.fetch(session, URL, params=params)

# constructing dict - handle exceptions later
try:
snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use loops! That would clean up this hunk of code

snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"]
snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"]
snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"]
snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"]
except KeyError:
snake_info["error"] = True
if snake_info["images"]:
i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/'
image_list = []
map_list = []
thumb_list = []

# Wikipedia has arbitrary images that are not snakes
banned = [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, hard coded lists should be moved to somewhere such as a class variable

'Commons-logo.svg',
'Red%20Pencil%20Icon.png',
'distribution',
'The%20Death%20of%20Cleopatra%20arthur.jpg',
'Head%20of%20holotype',
'locator',
'Woma.png',
'-map.',
'.svg',
'ange.',
'Adder%20(PSF).png'
]

for image in snake_info["images"]:
# images come in the format of `File:filename.extension`
file, sep, filename = image["title"].partition(':')
filename = filename.replace(" ", "%20") # Wikipedia returns good data!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the built-in urllib for this!


if not filename.startswith('Map'):
if any(ban in filename for ban in banned):
log.info("the image is banned")
else:
image_list.append(f"{i_url}{filename}")
thumb_list.append(f"{i_url}{filename}?width=100")
else:
map_list.append(f"{i_url}{filename}")

snake_info["image_list"] = image_list
snake_info["map_list"] = map_list
snake_info["thumb_list"] = thumb_list
return snake_info

@command(name="snakes.get()", aliases=["snakes.get"])
@bot_has_permissions(manage_messages=True)
@locked()
async def get(self, ctx: Context, name: Snake = None):
"""
Fetches information about a snake from Wikipedia.

: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
"""
if name is None:
name = Snake.random()

data = await self.get_snek(name)

if data.get('error'):
return await ctx.send('Could not fetch data from Wikipedia.')

match = self.wiki_brief.match(data['extract'])
embed = discord.Embed(
title=data['title'],
description=match.group(1) if match else None,
url=data['fullurl'],
colour=0x59982F
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What colour is this? Is it not included in the discord.Colour class as a staticmethod?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's just a random green I found online

)

fields = self.wiki_sects.findall(data['extract'])
excluded = ('see also', 'further reading', 'subspecies')

for title, body in fields:
if title.lower() in excluded:
continue
if not body.strip():
continue
# Only takes the first sentence
title, dot, _ = title.partition('.')
# There's probably a better way to do this
value = textwrap.shorten(body.strip(), width=200)
embed.add_field(name=title + dot, value=value + '\n\u200b', inline=False)

embed.set_footer(text='Powered by Wikipedia')

emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yak! Hard coded value inside the function again. Same issue as the list and dicts.

image = next((url for url in data['image_list'] if url.endswith(self.valid)), emoji)
embed.set_thumbnail(url=image)

await ctx.send(embed=embed)

@command(hidden=True)
async def zen(self, ctx):
"""
>>> import this

Long time Pythoneer Tim Peters succinctly channels the BDFL's guiding principles
for Python's design into 20 aphorisms, only 19 of which have been written down.

You must be connected to a voice channel in order to use this command.
"""
channel = ctx.author.voice.channel
if channel is None:
return

state = ctx.guild.voice_client
if state is not None:
# Already playing
return

voice = await channel.connect()
source = discord.FFmpegPCMAudio('zen.mp3')
voice.play(source, after=lambda *args: asyncio.run_coroutine_threadsafe(
voice.disconnect(), loop=ctx.bot.loop
))

@command(name="snakes.guess()", aliases=["snakes.guess", "identify"])
@locked()
async def guess(self, ctx):
"""
Snake identifying game!
"""
image = None

while image is None:
snakes = [Snake.random() for _ in range(5)]
answer = random.choice(snakes)

data = await self.get_snek(answer)

image = next((url for url in data['image_list'] if url.endswith(self.valid)), None)

embed = discord.Embed(
title='Which of the following is the snake in the image?',
colour=random.randint(1, 0xFFFFFF)
)
embed.set_image(url=image)

guess = await disambiguate(ctx, snakes, timeout=60, embed=embed)

# Any additional commands can be placed here. Be creative, but keep it to a reasonable amount!
if guess == answer:
return await ctx.send('You guessed correctly!')
await ctx.send(f'You guessed wrong. The correct answer was {answer}.')


def setup(bot):
Expand Down
Loading