Skip to content
This repository was archived by the owner on Mar 14, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
826e563
Merge pull request #1 from discord-python/master
katlyn Mar 23, 2018
03722c4
Sneks
katlyn Mar 23, 2018
8705049
Merge pull request #2 from discord-python/master
katlyn Mar 23, 2018
217839e
Added a word similarity detector.
silicWulf Mar 23, 2018
6b44d19
Add events.py to account for bad commands
katlyn Mar 23, 2018
99016fb
Silenced the screams of pain from flake8
silicWulf Mar 23, 2018
41b5c5c
Removed extra space from line 41
silicWulf Mar 23, 2018
95260a5
Added a newline
silicWulf Mar 23, 2018
300dad1
Add get_danger utility function
katlyn Mar 23, 2018
91cde5d
Add get_danger docstring
katlyn Mar 23, 2018
2055ced
Get ready for sneks
katlyn Mar 24, 2018
c3cf9a5
Line length fix
katlyn Mar 24, 2018
75f0154
Documented rattle.py
silicWulf Mar 24, 2018
1bde63d
Merge pull request #3 from discord-python/master
katlyn Mar 24, 2018
3ac3b81
Fix variable naming
katlyn Mar 24, 2018
8f74f6d
Added the get_snek core function, includes misspell detection and cor…
silicWulf Mar 24, 2018
21a0e5d
Added the snek database
silicWulf Mar 24, 2018
e927865
Fix some import errors
katlyn Mar 24, 2018
c761b7b
Added some debugging info, as well as a catch if spellcheck fails, an…
silicWulf Mar 24, 2018
40ecb63
Git screwed me... grrr
silicWulf Mar 24, 2018
2468e2b
how could I be so stupid?
silicWulf Mar 24, 2018
fa260c6
Fix embed building
katlyn Mar 24, 2018
68eaebd
Added an extra notice
silicWulf Mar 24, 2018
aecd752
I am so incredibly dense
silicWulf Mar 24, 2018
6c80c67
Reduced similarity threshold
silicWulf Mar 25, 2018
3fc366b
snek say
katlyn Mar 25, 2018
272a045
Fix flake8 not liking pickle
katlyn Mar 25, 2018
107af30
Fix whitespace
katlyn Mar 25, 2018
b3d26ee
Fixed spellcheck, removed shoddy descriptions
silicWulf Mar 25, 2018
9d2efc1
Fixed an error reeee
silicWulf Mar 25, 2018
904d416
Added some more info to the embed, fixed glaring issues
silicWulf Mar 25, 2018
5f9db70
Updated docstrings for fix_margins
silicWulf Mar 25, 2018
2837244
Fixed database probably, added some jokes
silicWulf Mar 25, 2018
8feee93
Fixed custom stuff
silicWulf Mar 25, 2018
4970c4f
Fix flake8 linting
katlyn Mar 25, 2018
3641d1b
Trying a cm fix
silicWulf Mar 25, 2018
d5fe34f
Add snek facts
katlyn Mar 25, 2018
1984019
Flake8 line too long
katlyn Mar 25, 2018
6d6f404
Added a facts command.
silicWulf Mar 25, 2018
67234de
Imported json...
silicWulf Mar 25, 2018
0b65319
Updated path I'm dumb
silicWulf Mar 25, 2018
503a4c0
Why do i keep doing this to myself
silicWulf Mar 25, 2018
721b941
Removed debug hardcode
silicWulf Mar 25, 2018
5def2bb
Renamed debug print to dprint
silicWulf Mar 25, 2018
a38d288
Added snek_pic command
silicWulf Mar 25, 2018
1f326ac
Ew fix
silicWulf Mar 25, 2018
fe8f102
n-nani?!
silicWulf Mar 25, 2018
0d78943
Fixed multi-lines
silicWulf 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
152 changes: 152 additions & 0 deletions bot/cogs/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# coding=utf-8
import logging

from discord import Embed, Member
from discord.ext.commands import (
AutoShardedBot, BadArgument, BotMissingPermissions,
CommandError, CommandInvokeError, Context,
NoPrivateMessage, UserInputError
)

from bot.constants import (
ADMIN_ROLE, DEVLOG_CHANNEL, DEVOPS_ROLE, MODERATOR_ROLE, OWNER_ROLE, PYTHON_GUILD, SITE_API_KEY, SITE_API_USER_URL
)

log = logging.getLogger(__name__)


class Events:
"""
No commands, just event handlers
"""

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

async def send_updated_users(self, *users):
try:
response = await self.bot.http_session.post(
url=SITE_API_USER_URL,
json=list(users),
headers={"X-API-Key": SITE_API_KEY}
)

return await response.json()
except Exception as e:
log.error(f"Failed to send role updates: {e}")
return {}

async def on_command_error(self, ctx: Context, e: CommandError):
command = ctx.command
parent = None

if command is not None:
parent = command.parent

if parent and command:
help_command = (self.bot.get_command("help"), parent.name, command.name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Including the ( and ) is unnecessary, you can accomplish the same feat with simply

help_command = self.bot.get_command("help"), parent.name, command.name

elif command:
help_command = (self.bot.get_command("help"), command.name)
else:
help_command = (self.bot.get_command("help"),)

if isinstance(e, BadArgument):
await ctx.send(f"Bad argument: {e}\n")
await ctx.invoke(*help_command)
elif isinstance(e, UserInputError):
await ctx.invoke(*help_command)
elif isinstance(e, NoPrivateMessage):
await ctx.send("Sorry, this command can't be used in a private message!")
elif isinstance(e, BotMissingPermissions):
await ctx.send(
f"Sorry, it looks like I don't have the permissions I need to do that.\n\n"
f"Here's what I'm missing: **{e.missing_perms}**"
)
elif isinstance(e, CommandInvokeError):
await ctx.send(
f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```"
)
raise e.original
log.error(f"COMMAND ERROR: '{e}'")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This isn't logged if a non-expected CommandInvokeError happens. You could either wrap the ifs in a try - finally block, however just logging the error before you raise it would be fine.


async def on_ready(self):
users = []

for member in self.bot.get_guild(PYTHON_GUILD).members: # type: Member
roles = [r.id for r in member.roles] # type: List[int]

if OWNER_ROLE in roles:
users.append({
"user_id": member.id,
"role": OWNER_ROLE
})
elif ADMIN_ROLE in roles:
users.append({
"user_id": member.id,
"role": ADMIN_ROLE
})
elif MODERATOR_ROLE in roles:
users.append({
"user_id": member.id,
"role": MODERATOR_ROLE
})
elif DEVOPS_ROLE in roles:
users.append({
"user_id": member.id,
"role": DEVOPS_ROLE
})

if users:
log.debug(f"{len(users)} user roles updated")
data = await self.send_updated_users(*users) # type: dict

if any(data.values()):
embed = Embed(
title="User roles updated"
)

for key, value in data.items():
if value:
embed.add_field(
name=key.title(), value=str(value)
)

await self.bot.get_channel(DEVLOG_CHANNEL).send(
embed=embed
)

async def on_member_update(self, before: Member, after: Member):
if before.roles == after.roles:
return

before_role_names = [role.name for role in before.roles] # type: List[str]
after_role_names = [role.name for role in after.roles] # type: List[str]
role_ids = [r.id for r in after.roles] # type: List[int]

log.debug(f"{before.display_name} roles changing from {before_role_names} to {after_role_names}")

if OWNER_ROLE in role_ids:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You do this process twice, you should make a function that does this. Don't repeat yourself 😉

self.send_updated_users({
"user_id": after.id,
"role": OWNER_ROLE
})
elif ADMIN_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": ADMIN_ROLE
})
elif MODERATOR_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": MODERATOR_ROLE
})
elif DEVOPS_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": DEVOPS_ROLE
})


def setup(bot):
bot.add_cog(Events(bot))
log.info("Cog loaded: Events")
81 changes: 81 additions & 0 deletions bot/cogs/snakes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
# coding=utf-8
import logging
from copy import copy
from pickle import load
from random import choice
from typing import Any, Dict

from discord import Embed
from discord.ext.commands import AutoShardedBot, Context, command

from ..tools import rattle
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please no relative imports.



log = logging.getLogger(__name__)
db = load(open('bot/cogs/snek.pickledb', 'rb')) # are we going to move this db elsewhere?
SNAKE_NAMES = db.keys() # make a list of common names for snakes, used for random snake and autocorrect
DEBUG = True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hardcoded debug mode?

print = print if DEBUG else lambda *a, **k: None


class NoGuessError(Exception):
def __init__(self, message='', debugdata=None):
self.message = message
self.debugdata = debugdata
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is python, let's use _ where there should be a space:

class NoGuessError(Exception):
    def __init__(self, message='', debug_data=None):
        self.message = message
        self.debug_data = debug_data



class Snakes:
Expand All @@ -28,6 +45,55 @@ 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 is None: # if it's None
name = choice(SNAKE_NAMES) # get random key (common) name
src = db[name] # source of info = db[common name]
else:
try:
name = name.lower() # lowercase the name for hitting dict
src = db[name] # source of info = db[common name]
except KeyError: # if name not found...
possible_misspellings = list(rattle.check_word(name, SNAKE_NAMES, threshold=0.19)) # get similars
possible_misspellings = sorted(possible_misspellings, key=lambda x: x[0]) # sort the list
possible_misspellings = list(reversed(possible_misspellings)) # reverse it
'''
just a thought, should we check if the next command request from the same person goes from a possibility
rate from, say, 0.5 to 1.0 (50% accuracy to 100%) so that we can cache known misspellings?
'''
try:
src = await self.get_snek(possible_misspellings[0][1]) # recurse/refine
name = src['common name']
except IndexError: # no guesses on misspellings
raise NoGuessError(debugdata='requested = {}'.format(name))
except ValueError:
raise ValueError('snek not found')

info = copy(src) # make a copy of the dictionary
info['common name'] = name # make common name key
return info

async def get_danger(self, level: str = None) -> str:
"""
Returns the human-readable version of the danger level.
:param level: The danger level of a snek
:return: A string that is the human readable version of passed level.
"""
return {
'???': 'Danger unknown',
'---': 'Nonvenomous',
'??🐍': 'Constrictor, danger unknown',
'🐍': 'Constrictor, considered harmless',
'🐍🐍': 'Constrictor, harmful',
'🐍🐍🐍': 'Constrictor, dangerous',
'🐍🐍🐍🐍': 'Constrictor, very dangerous',
'🐍🐍🐍🐍🐍': 'Constrictor, extremely damgerous',
'??💀': 'Venomous, danger unknown',
'💀': 'Venomous, considered harmless',
'💀💀': 'Venomous, harmful',
'💀💀💀': 'Venomous, dangerous',
'💀💀💀💀': 'Venomous, very dangerous',
'💀💀💀💀💀': 'Venomous, extremely dangerous.'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could this be moved into a json file, that's already loaded?

}.get(level, 'Unknown')

@command()
async def get(self, ctx: Context, name: str = None):
Expand All @@ -41,6 +107,21 @@ async def get(self, ctx: Context, name: str = None):
:param name: Optional, the name of the snake to get information for - omit for a random snake
"""

try:
snek = await self.get_snek(name)
except NoGuessError as e:
print('debug: {}'.format(e.debugdata))
await ctx.send("I'm sorry, I don't know what you requested.")

embed = Embed(title=snek.get('common name'), description=snek.get('description'))
# Commented out until I know what information I have to use.
# embed.add_field(name="More Information", value="```Species | xxx\rGenus | xxx\rFamily | xxx```")
embed.add_field(name=snek.get('rating'), value=await self.get_danger(snek.get('rating')), inline=True)
embed.set_image(url=snek.get('image'))
embed.set_footer(text="Information from Wikipedia and snakedatabase.org. Information has been " \
"automatically fetched and may not be accurate.")
await ctx.send(embed=embed)

# Any additional commands can be placed here. Be creative, but keep it to a reasonable amount!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With 12 hours to go, you have no extra functionality!



Expand Down
Binary file added bot/cogs/snek.pickledb
Binary file not shown.
64 changes: 64 additions & 0 deletions bot/tools/rattle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# rattle - easy word similarity detection
def check_word(word: str, wordset: list, threshold: float=0.0):
'''Checks a word for its similarity to a list of words.

:param word: A word to compare to a list of words.
:param wordset: An iterable value that `word` will be compared to.
:param threshold: Optional, only yields similarity values and words that are above the given threshold
(1.0 is 100% similarity, 0.45 is 45% similarity, etc.)
:yield: A similarity value and the word corresponding with it.'''
def get_shortest(x, y):
'''Gets the shortest string between x and y.

If both strings are of equal length, x will be returned. (This does not matter in the wrapping function)

:param x: A string.
:param y: Another string.
:return: The string that is shorter than the other.'''
return x if len(x) < len(y) else y

def match_length(word, otherword):
'''Append characters to the end of word to meet the length of otherword.

:param word: A shorter word.
:param otherword: A longer word.
:return: The shorter word but with extra characters to meet the length of the longer word.'''
h = len(otherword) - len(word)
word += '~' * h
return word
'''
glossary:
shortest - shortest string
count - counter used to get similarity
length_diff_multiplier - multiplier for extra characters
mp_count - multiplier for the multiplier, increases on every extra space
shorter - string used as length-matched word
longer - string used as *not* `o` (is longer)
'''
for w in wordset: # check against every word
shortest = get_shortest(w, word) # get the shortest word
count = 0 # prepare the similarity counter
length_diff_multiplier = 1.08 # set the length difference multiplier
mp_count = 1 # set the current lendiff count
shorter = '' # prepare a variable for readability
longer = '' # prepare another variable for readability
if shortest == word: # if the shortest word is the given word...
shorter = match_length(word, w) # lengthen the given word to match the longer one to avoid IndexErrors
longer = w # set the longer word to the cycled word
else: # if the shortest word is the cycled word...
shorter = match_length(w, word) # length the cycled word to match ''[ditto]
longer = word # set longer word to the given word

for i, char in enumerate(longer): # for each character and index in the longer word...
if char != shorter[i]: # if the characters aren't the same
if char == '~': # if the character is an extra space
count += mp_count * length_diff_multiplier # increase the count by the current multiplier
mp_count += 1 # increase the "multiplier multiplier" by one
else: # if it's just a character difference
count += 1 # increase the count by one

sim = 1.0 - (count / len(shorter)) # get the similarity
if sim >= threshold: # if the similarity is above the threshold...
yield sim, w # yield the similarity and the cycled word
else: # otherwise...
continue # continue on with your life