-
-
Notifications
You must be signed in to change notification settings - Fork 16
Team 18 #7
base: master
Are you sure you want to change the base?
Team 18 #7
Changes from 25 commits
826e563
03722c4
8705049
217839e
6b44d19
99016fb
41b5c5c
95260a5
300dad1
91cde5d
2055ced
c3cf9a5
75f0154
1bde63d
3ac3b81
8f74f6d
21a0e5d
e927865
c761b7b
40ecb63
2468e2b
fa260c6
68eaebd
aecd752
6c80c67
3fc366b
272a045
107af30
b3d26ee
9d2efc1
904d416
5f9db70
2837244
8feee93
4970c4f
3641d1b
d5fe34f
1984019
6d6f404
67234de
0b65319
503a4c0
721b941
5def2bb
a38d288
1f326ac
fe8f102
0d78943
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| 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}'") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is python, let's use class NoGuessError(Exception):
def __init__(self, message='', debug_data=None):
self.message = message
self.debug_data = debug_data |
||
|
|
||
|
|
||
| class Snakes: | ||
|
|
@@ -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.' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
|
@@ -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! | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With 12 hours to go, you have no extra functionality! |
||
|
|
||
|
|
||
|
|
||
| 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 |
There was a problem hiding this comment.
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