From 803d25d67f0e934900192be3c96ef9be8ea92abd Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Wed, 21 Nov 2018 20:09:30 -0600 Subject: [PATCH] Added command API and bot +Rewrote all of Geoffrey's commands for Django +Command api allows all the geoffrey commands to be accessed though a REST API +Added foundation for getting bot to work, no commands at the moment --- api/__init__.py | 0 api/bot/BotErrors.py | 87 ++++++++++ api/bot/DiscordHelperFunctions.py | 29 ++++ api/bot/MinecraftAccountInfoGrabber.py | 36 ++++ api/bot/__init__.py | 0 api/bot/bot.py | 167 ++++++++++++++++++ api/bot/cogs/Add_Commands.py | 146 ++++++++++++++++ api/bot/cogs/Admin_Commands.py | 166 ++++++++++++++++++ api/bot/cogs/Delete_Commands.py | 65 +++++++ api/bot/cogs/Edit_Commands.py | 76 +++++++++ api/bot/cogs/Search_Commands.py | 139 +++++++++++++++ api/bot/cogs/__init__.py | 1 + api/commands.py | 228 +++++++++++++++++++++++++ api/urls.py | 7 + api/views.py | 36 ++++ apps.py | 5 - assets/bots/geoffrey.py | 14 ++ migrations/0001_initial.py | 14 +- models.py | 24 ++- views.py | 3 + 20 files changed, 1227 insertions(+), 16 deletions(-) create mode 100644 api/__init__.py create mode 100644 api/bot/BotErrors.py create mode 100644 api/bot/DiscordHelperFunctions.py create mode 100644 api/bot/MinecraftAccountInfoGrabber.py create mode 100644 api/bot/__init__.py create mode 100644 api/bot/bot.py create mode 100644 api/bot/cogs/Add_Commands.py create mode 100644 api/bot/cogs/Admin_Commands.py create mode 100644 api/bot/cogs/Delete_Commands.py create mode 100644 api/bot/cogs/Edit_Commands.py create mode 100644 api/bot/cogs/Search_Commands.py create mode 100644 api/bot/cogs/__init__.py create mode 100644 api/commands.py create mode 100644 api/urls.py create mode 100644 api/views.py delete mode 100644 apps.py create mode 100755 assets/bots/geoffrey.py diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/bot/BotErrors.py b/api/bot/BotErrors.py new file mode 100644 index 0000000..2f6b53d --- /dev/null +++ b/api/bot/BotErrors.py @@ -0,0 +1,87 @@ +class DataBaseError(Exception): + """Base class for exceptions in this module.""" + pass + + +class LocationInitError(DataBaseError): + """Error in initializing Location""" + + +class TunnelInitError(DataBaseError): + """Error in initializing Tunnel""" + + +class NoMatchFoundError(DataBaseError): + """No matches were found in the database""" + + +class LocationLookUpError(DataBaseError): + """Error in finding location in database""" + + +class DeleteEntryError(DataBaseError): + """Error in deleting entry""" + + +class UsernameLookupFailed(Exception): + """Error in username lookup, is the player's nickname set correctly? *stares at aeskdar*""" + + +class PlayerNotFound(DataBaseError): + """Player not found in database.""" + + +class EntryNameNotUniqueError(DataBaseError): + """A location by that name is already in the database.""" + + +class StringTooLong(DataBaseError): + """Given string is too long.""" + + +class DatabaseValueError(DataBaseError): + """'String too long or number too large""" + + +class ItemNotFound(DataBaseError): + """No item matches found in database""" + + +class InvalidDimError(DataBaseError): + """Invalid dimension name""" + + +class InvalidTunnelError(DataBaseError): + """Invalid tunnel name""" + + +class PlayerInDBError(DataBaseError): + """Player already registered in database""" + + +class LocationHasTunnelError(DataBaseError): + """That location already has a tunnel""" + + +class NoPermissionError(DataBaseError): + """You have no permission to run this command""" + + +class NotOnServerError(DataBaseError): + """You need to run this command on 24CC""" + + +class NoLocationsInDatabase(DataBaseError): + """This player has no locations in the database""" + + +class FuckyWucky: + """You made one.""" + + +class EmptryString(DataBaseError): + """Empty string provided""" + + +class CommandNotFound(DataBaseError): + """Command not found""" diff --git a/api/bot/DiscordHelperFunctions.py b/api/bot/DiscordHelperFunctions.py new file mode 100644 index 0000000..6fb27ae --- /dev/null +++ b/api/bot/DiscordHelperFunctions.py @@ -0,0 +1,29 @@ +from itertools import zip_longest + + +def get_name(args): + if len(args) > 0: + name = ' '.join(args) + else: + name = None + + return name + + +def get_nickname(discord_user, special_users): + if discord_user.nick is None: + name = discord_user.display_name + else: + name = discord_user.nick + + if name in special_users: + return special_users[name] + else: + return name + + +def get_args_dict(args): + if len(args) != 0: + return dict(zip_longest(*[iter(args)] * 2, fillvalue=" ")) + else: + return {} diff --git a/api/bot/MinecraftAccountInfoGrabber.py b/api/bot/MinecraftAccountInfoGrabber.py new file mode 100644 index 0000000..d39c483 --- /dev/null +++ b/api/bot/MinecraftAccountInfoGrabber.py @@ -0,0 +1,36 @@ +from simplejson.errors import JSONDecodeError + +import requests + +from GeoffreyApp.api.bot.BotErrors import UsernameLookupFailed + +uuid_lookup_url = 'https://api.mojang.com/users/profiles/minecraft/{}' +username_lookup_url = 'https://api.mojang.com/user/profiles/{}/names' + + +def grab_json(url): + try: + json = requests.get(url).json() + if 'error' in json: + raise UsernameLookupFailed + + except JSONDecodeError: + raise UsernameLookupFailed + + return json + + +def grab_UUID(username): + player_data = grab_json(uuid_lookup_url.format(username)) + return player_data['id'] + + +def grab_playername(uuid): + player_data = grab_json(username_lookup_url.format(uuid)) + + if len(player_data) == 0: + raise UsernameLookupFailed + else: + last_index = len(player_data) - 1 + + return player_data[last_index]['name'] diff --git a/api/bot/__init__.py b/api/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/bot/bot.py b/api/bot/bot.py new file mode 100644 index 0000000..9e17ba1 --- /dev/null +++ b/api/bot/bot.py @@ -0,0 +1,167 @@ +import asyncio +import logging + +from discord import Game +from discord.ext import commands +from discord.utils import oauth_url +import logging.handlers as handlers +from sys import stdout +from os import path + +from GeoffreyApp.api.bot.BotErrors import * +from GeoffreyApp.api.commands import * +from django.conf import settings + +logger = logging.getLogger(__name__) + +description = ''' +Geoffrey (pronounced JOFF-ree) started his life as an inside joke none of you will understand. +At some point, she was to become an airhorn bot. Now, they know where your stuff is. + +Please respect Geoffrey, the bot is very sensitive. + +All commands must be prefaced with '?' + +If have a suggestion or if something is borked, you can PM my ding dong of a creator BirbHD. + +*You must use ?register before adding things to Geoffrey* + +For a better a explanation on how this bot works go the following link: +https://github.com/joeyahines/Geoffrey/blob/master/README.md + +''' + +bad_error_message = 'OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The admins at our ' \ + 'headquarters are working VEWY HAWD to fix this! (Error in command {})' + +extensions = [] +''' +extensions = ['GeoffreyApp.cogs.Add_Commands', + 'GeoffreyApp.cogs.Delete_Commands', + 'GeoffreyApp.cogs.Edit_Commands', + 'GeoffreyApp.cogs.Search_Commands', + 'GeoffreyApp.cogs.Admin_Commands'] +''' + + +class GeoffreyBot(commands.Bot): + def __init__(self): + super().__init__(command_prefix=getattr(settings, 'BOT_PREFIX', '?'), description=description, pm_help=True, case_insensitive=True) + self.error_users = getattr(settings, 'ERROR_USERS', []) + self.admin_users = getattr(settings, 'MOD_RANK', []) + self.special_users = getattr(settings, 'SPECIAL_USERS', []) + self.default_status = getattr(settings, 'DEFAULT_STATUS', 'sed') + + for extension in extensions: + try: + self.load_extension(extension) + except Exception as e: + logger.info('Failed to load extension {}'.format(extension)) + raise e + + async def on_ready(self): + logger.info("%s Online, ID: %s", self.user.name, self.user.id) + info = await self.application_info() + url = oauth_url(info.id) + logger.info("Bot url: %s", url) + await self.change_presence(activity=Game(self.default_status)) + + async def on_command(self, ctx): + if ctx.invoked_subcommand is None: + subcommand = "" + else: + subcommand = ":" + ctx.invoked_subcommand.__str__() + + logger.info("User %s, used command %s%s with context: %s", ctx.message.author, ctx.command.name, subcommand, + ctx.args) + + if ctx.invoked_with.lower() == 'help' and ctx.message.guild is not None: + await ctx.send("{}, I sent you some help in the DMs.".format(ctx.message.author.mention)) + + async def on_command_error(self, ctx, error): + error_str = '' + if hasattr(ctx, 'cog'): + if "Admin_Commands" in ctx.cog.__str__(): + return + if hasattr(error, 'original'): + if isinstance(error.original, NoPermissionError): + error_str = 'You don\'t have permission for that cool command.' + elif isinstance(error.original, UsernameLookupFailed): + error_str = 'Your user name was not found, either Mojang is having a fucky wucky ' \ + 'or your nickname is not set correctly. *stares at the Mods*' + elif isinstance(error.original, PlayerNotFound): + error_str = 'Make sure to use ?register first you ding dong.' + elif isinstance(error.original, EntryNameNotUniqueError): + error_str = 'An entry in the database already has that name you ding dong.' + elif isinstance(error.original, DatabaseValueError): + error_str = 'Use a shorter name or a smaller value, dong ding.' + elif isinstance(error.original, NotOnServerError): + error_str = 'Command needs to be run on 24CC. Run this command there whoever you are.'.format() + elif isinstance(error.original, EmptryString): + error_str = 'Do not not pass empty string to Geoffrey. Ding dong.' + elif isinstance(error, commands.CommandOnCooldown): + return + elif isinstance(error, commands.UserInputError): + error_str = 'Invalid syntax for **{}** you ding dong:' \ + .format(ctx.invoked_with, ctx.invoked_with) + + pages = await self.formatter.format_help_for(ctx, ctx.command) + for page in pages: + error_str = error_str + '\n' + page + elif isinstance(error, commands.CommandNotFound): + return + + if error_str is '': + await self.send_error_message( + 'Geoffrey encountered unhandled exception: {} Command: **{}** Context: {}'.format(error, + ctx.command.name, + ctx.args)) + error_str = bad_error_message.format(ctx.invoked_with) + + logger.error("Geoffrey encountered exception: %s", error) + + await ctx.message.channel.send('{} **Error Running Command:** {}'.format( + ctx.message.author.mention, error_str)) + + async def send_error_message(self, msg): + for user_id in self.error_users: + user = await self.get_user_info(user_id) + await user.send(msg) + + +def setup_logging(): + discord_logger = logging.getLogger('discord') + discord_logger.setLevel(logging.INFO) + bot_info_logger = logging.getLogger('GeoffreyApp.api.bot.bot') + bot_info_logger.setLevel(logging.INFO) + console = logging.StreamHandler(stdout) + console.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) + + bot_info_logger.addHandler(console) + + +def start_bot(): + bot = None + try: + bot = GeoffreyBot() + + @bot.command(pass_context=True) + async def test(ctx): + """ + Checks if the bot is alive. + """ + await ctx.send('I\'m here you ding dong') + + setup_logging() + + bot.run(getattr(settings, 'DISCORD_TOKEN')) + + except KeyboardInterrupt: + logger.info("Bot received keyboard interrupt") + except Exception as e: + print(e) + logger.info('Bot encountered the following unhandled exception %s', e) + finally: + if bot is not None: + bot.loop.stop() + logger.info("Bot shutting down...") \ No newline at end of file diff --git a/api/bot/cogs/Add_Commands.py b/api/bot/cogs/Add_Commands.py new file mode 100644 index 0000000..f584cd1 --- /dev/null +++ b/api/bot/cogs/Add_Commands.py @@ -0,0 +1,146 @@ +from discord.ext import commands + +from geoffrey.BotErrors import * +from geoffrey.DiscordHelperFunctions import * + + +@commands.cooldown(5, 60, commands.BucketType.user) +class Add_Commands: + """ + Commands for adding things to Geoffrey. + *You must use ?register before using any of these commands!* + """ + + def __init__(self, bot): + self.bot = bot + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def register(self, ctx): + """ + Registers your Discord and Minecraft account with the the database + You must do this before adding entries to the database. + """ + + try: + player_name = get_nickname(ctx.message.author, self.bot.special_users) + self.bot.bot_commands.register(player_name, ctx.message.author.id) + await ctx.send('{}, you have been added to the database. Use ?help to see all the commands this bot can do.' + .format(ctx.message.author.mention)) + except AttributeError: + raise NotOnServerError + except PlayerInDBError: + await ctx.send('{}, you are already in the database. Ding dong.'.format(ctx.message.author.mention)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def add_base(self, ctx, x_pos: int, z_pos: int, *args): + """ + Adds your base to the database. The base name is optional if this is your first base + ?add_base [X Coordinate] [Z Coordinate] [Base Name] + """ + + name = get_name(args) + + try: + base = self.bot.bot_commands.add_base(x_pos, z_pos, base_name=name, discord_uuid=ctx.message.author.id) + await ctx.send( + '{}, your base has been added to the database: \n\n{}'.format(ctx.message.author.mention, base)) + except LocationInitError: + raise commands.UserInputError + except EntryNameNotUniqueError: + if name is None: + await ctx.send('{}, you already have one base in the database, you need to specify a base' + ' name'.format(ctx.message.author.mention)) + else: + await ctx.send( + '{}, a base called **{}** already exists. You need to specify a different name.'.format( + ctx.message.author.mention, name)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def add_shop(self, ctx, x_pos: int, z_pos: int, *args): + """ + Adds your shop to the database. The name is optional if this is your first shop + ?add_shop [X Coordinate] [Z Coordinate] [Shop Name] + """ + + name = get_name(args) + + try: + shop = self.bot.bot_commands.add_shop(x_pos, z_pos, shop_name=name, discord_uuid=ctx.message.author.id) + await ctx.send( + '{}, your shop has been added to the database: \n\n{}'.format(ctx.message.author.mention, shop)) + except LocationInitError: + raise commands.UserInputError + except EntryNameNotUniqueError: + if name is None: + await ctx.send( + '{}, you already have one shop in the database, you need to specify a shop name'.format( + ctx.message.author.mention)) + else: + await ctx.send( + '{}, a shop called **{}** already exists. You need to specify a different name.'.format( + ctx.message.author.mention, name)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def add_tunnel(self, ctx, tunnel_direction: str, tunnel_number: int, *args): + """ + Adds your tunnel to the database. If you only have one location, you do not need to specify a location name + + Directions: North South East West + ?tunnel [Tunnel Direction] [Tunnel Number] [Location Name] + """ + + loc_name = get_name(args) + try: + self.bot.bot_commands.add_tunnel(tunnel_direction, tunnel_number, discord_uuid=ctx.message.author.id, + location_name=loc_name) + await ctx.send('{}, your tunnel has been added to the database'.format(ctx.message.author.mention)) + except LocationLookUpError: + await ctx.send('{}, you do not have a location called **{}**.'.format( + ctx.message.author.mention, loc_name)) + except NoLocationsInDatabase: + await ctx.send('{}, you do not have a location in the database.'.format( + ctx.message.author.mention, loc_name)) + except LocationHasTunnelError: + await ctx.send('{}, **{}** already has a tunnel.'.format(ctx.message.author.mention, loc_name)) + except TunnelInitError: + await ctx.send('{}, invalid tunnel direction.'.format(ctx.message.author.mention)) + except EntryNameNotUniqueError: + await ctx.send('{}, you have more than one location, you need to specify a location.' + .format(ctx.message.author.mention)) + except InvalidTunnelError: + await ctx.send( + '{}, **{}** is an invalid tunnel direction.'.format(ctx.message.author.mention, tunnel_direction)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def add_item(self, ctx, item_name: str, quantity: int, diamond_price: int, *args): + """ + Adds an item to a shop's inventory. If you have one shop, the shop name is not required + + Quantity for Diamond Price. eg. 32 Dirt for 1D. If the item name has spaces in wrap in in quotes eg "Silk Touch" + ?add_item [Item Name] [Quantity] [Price] [Shop name] + """ + shop_name = get_name(args) + try: + self.bot.bot_commands.add_item(item_name, quantity, diamond_price, shop_name=shop_name, + discord_uuid=ctx.message.author.id) + await ctx.send( + '{}, **{}** has been added to the inventory of your shop.'.format(ctx.message.author.mention, + item_name)) + except NoLocationsInDatabase: + await ctx.send('{}, you don\'t have any shops in the database.'.format(ctx.message.author.mention)) + except EntryNameNotUniqueError: + await ctx.send('{}, you have more than one shop in the database, please specify a shop name.' + .format(ctx.message.author.mention)) + except LocationLookUpError: + await ctx.send( + '{}, you don\'t have any shops named **{}** in the database.'.format(ctx.message.author.mention, + shop_name)) + + +def setup(bot): + bot.add_cog(Add_Commands(bot)) diff --git a/api/bot/cogs/Admin_Commands.py b/api/bot/cogs/Admin_Commands.py new file mode 100644 index 0000000..50c91aa --- /dev/null +++ b/api/bot/cogs/Admin_Commands.py @@ -0,0 +1,166 @@ +from discord import Game +from discord.ext import commands + +from geoffrey.BotErrors import * +from geoffrey.DiscordHelperFunctions import get_name + + +def check_mod(user, admin_users): + try: + for role in user.roles: + if str(role.id) in admin_users: + return True + except AttributeError: + raise NotOnServerError + + return False + + +class Admin_Commands: + """ + Commands for cool people only. + """ + + def __init__(self, bot): + self.bot = bot + + async def error(self, ctx, error): + error_str = "" + + if hasattr(error, "original"): + if isinstance(error.original, PlayerNotFound): + error_str = 'that player is not in the database.' + elif isinstance(error.original, DeleteEntryError) or isinstance(error.original, LocationLookUpError): + error_str = 'that player does not have a location by that name.' + + if error_str is "": + error_str = 'the bot encountered the following error: {}'.format(error.__str__()) + + await ctx.send('{}, {}'.format(ctx.message.author.mention, error_str)) + + @commands.command(pass_context=True) + async def test(self, ctx): + """ + Checks if the bot is alive. + """ + if check_mod(ctx.message.author, self.bot.admin_users): + await ctx.send('I\'m here you ding dong') + else: + raise NoPermissionError + + @commands.group(pass_context=True) + async def mod(self, ctx): + """ + Bot moderation tools. + """ + if check_mod(ctx.message.author, self.bot.admin_users): + if ctx.invoked_subcommand is None: + await ctx.send('{}, invalid sub-command for command **mod**.'.format(ctx.message.author.mention)) + else: + raise NoPermissionError + + @mod.command(pass_context=True) + async def delete(self, ctx, discord_uuid: str, location_name: str): + """ + Deletes a location in the database. + """ + self.bot.bot_commands.delete(location_name, discord_uuid=discord_uuid) + await ctx.send('{}, **{}** has been deleted.'.format(ctx.message.author.mention, location_name)) + + @delete.error + async def delete_error(self, ctx, error): + await self.error(ctx, error) + + @mod.command(pass_context=True) + async def edit_name(self, ctx, discord_uuid: str, new_name: str, current_name: str): + """ + Edits the name of a location in the database. + """ + self.bot.bot_commands.edit_name(new_name, current_name, discord_uuid=discord_uuid) + await ctx.send('{}, **{}** has been rename to **{}**.'.format(ctx.message.author.mention, current_name, + new_name)) + + @edit_name.error + async def edit_error(self, ctx, error): + await self.error(ctx, error) + + @mod.command(pass_context=True) + async def update_mc_uuid(self, ctx, discord_uuid: str, mc_uuid: str): + """ + Updates a user's MC UUID + """ + self.bot.bot_commands.update_mc_uuid(discord_uuid, mc_uuid) + await ctx.send('{}, **{}** has been updated.'.format(ctx.message.author.mention, discord_uuid)) + + @update_mc_uuid.error + async def update_mc_uuid_error(self, ctx, error): + await self.error(ctx, error) + + @mod.command(pass_context=True) + async def update_discord_uuid(self, ctx, new_discord_uuid: str, current_discord_uuid: str): + """ + Updates a user's Discord UUID + """ + self.bot.bot_commands.update_mc_uuid(current_discord_uuid, new_discord_uuid) + await ctx.send('{}, user **{}** has been updated.'.format(ctx.message.author.mention, current_discord_uuid)) + + @update_discord_uuid.error + async def update_discord_uuid_error(self, ctx, error): + await self.error(ctx, error) + + @mod.command(pass_context=True) + async def update_mc_name(self, ctx, discord_uuid: str): + """ + Updates a user's MC name to the current name on the MC UUID + """ + self.bot.bot_commands.update_mc_name(discord_uuid) + await ctx.send('{}, user **{}**\'s MC name has update.'.format(ctx.message.author.mention, discord_uuid)) + + @update_mc_name.error + async def update_mc_name_error(self, ctx, error): + await self.error(ctx, error) + + @mod.command(pass_context=True) + async def status(self, ctx, *args): + """ + Updates "playing [game]" status of the bot + """ + status = get_name(args) + await self.bot.change_presence(activity=Game(status)) + await ctx.send('{}, status has been changed'.format(ctx.message.author.mention)) + + @mod.command(pass_context=True) + async def add_player(self, ctx, discord_uuid: str, mc_name: str): + """ + Manually add a player to the database + """ + try: + db_id = self.bot.bot_commands.add_player(discord_uuid, mc_name) + await ctx.send('{}, user **{}** been added to the data base with id {}.'.format(ctx.message.author.mention, + mc_name, db_id)) + except PlayerInDBError: + await ctx.send('{}, user **{}** is already in the database.'.format(ctx.message.author.mention, mc_name)) + + @add_player.error + async def add_player_error(self, ctx, error): + await self.error(ctx, error) + + @mod.command(pass_context=True) + async def find_player(self, ctx, discord_uuid: str): + """ + Finds a player in the database + """ + try: + db_id, username, discord_uuid, minecraft_uuid = self.bot.bot_commands.find_player(discord_uuid) + await ctx.send('Username: {}, id: {}, Discord UUID: {}, Minecraft UUID: {}' + .format(username, db_id, discord_uuid, minecraft_uuid)) + except PlayerNotFound: + await ctx.send('That player is not in the database...') + + @find_player.error + async def find_player_error(self, ctx, error): + await self.error(ctx, error) + + +def setup(bot): + bot.add_cog(Admin_Commands(bot)) diff --git a/api/bot/cogs/Delete_Commands.py b/api/bot/cogs/Delete_Commands.py new file mode 100644 index 0000000..3010262 --- /dev/null +++ b/api/bot/cogs/Delete_Commands.py @@ -0,0 +1,65 @@ +from discord.ext import commands + +from geoffrey.BotErrors import * +from geoffrey.DiscordHelperFunctions import * + + +class Delete_Commands: + """ + Commands to help Geoffrey forget. + + *You must use ?register before using any of these commands!* + """ + + def __init__(self, bot): + self.bot = bot + + @commands.command(pass_context=True) + async def delete(self, ctx, *args): + """ + Deletes a location from the database + + ?delete [Location name] + """ + loc = get_name(args) + try: + if loc is None: + raise commands.UserInputError + + self.bot.bot_commands.delete(loc, discord_uuid=ctx.message.author.id) + await ctx.send( + '{}, your location named **{}** has been deleted.'.format(ctx.message.author.mention, loc)) + except (DeleteEntryError, PlayerNotFound): + await ctx.send('{}, you do not have a location named **{}**.'.format(ctx.message.author.mention, loc)) + + @commands.command(pass_context=True) + async def delete_item(self, ctx, item: str, *args): + """ + Deletes an item listing from a shop inventory + + The item name must be wrapped in quotes if it has a space in it + ?delete_name [Item] [Shop Name] + """ + + shop = get_name(args) + try: + shop_name = self.bot.bot_commands.delete_item(item, shop, discord_uuid=ctx.message.author.id) + + await ctx.send('{}, **{}** has been removed from the inventory of **{}**.'. + format(ctx.message.author.mention, item, shop_name)) + except LocationLookUpError: + await ctx.send('{}, you do not have a shop called **{}**.'.format(ctx.message.author.mention, shop)) + except NoLocationsInDatabase: + await ctx.send('{}, you do have any shops in the database.'.format(ctx.message.author.mention)) + except EntryNameNotUniqueError: + await ctx.send('{}, you have more than one shop in the database, please specify a shop name.' + .format(ctx.message.author.mention)) + except DeleteEntryError: + if shop is not None: + await ctx.send('{}, **{}** does not sell **{}**.'.format(ctx.message.author.mention, shop, item)) + else: + await ctx.send('{}, your shop does not sell **{}**.'.format(ctx.message.author.mention, item)) + + +def setup(bot): + bot.add_cog(Delete_Commands(bot)) diff --git a/api/bot/cogs/Edit_Commands.py b/api/bot/cogs/Edit_Commands.py new file mode 100644 index 0000000..c032dd3 --- /dev/null +++ b/api/bot/cogs/Edit_Commands.py @@ -0,0 +1,76 @@ +from discord.ext import commands + +from geoffrey.BotErrors import * +from geoffrey.DiscordHelperFunctions import * + + +class Edit_Commands: + """ + Commands for editing your stuff in Geoffrey. + """ + + def __init__(self, bot): + self.bot = bot + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def edit_pos(self, ctx, x_pos: int, z_pos: int, *args): + """ + Edits the position of a location + + ?edit_pos [X Coordinate] [Z Coordinate] [Location Name] + """ + loc = get_name(args) + try: + loc_str = self.bot.bot_commands.edit_pos(x_pos, z_pos, loc, discord_uuid=ctx.message.author.id) + + await ctx.send( + '{}, the following location has been updated: \n\n{}'.format(ctx.message.author.mention, loc_str)) + except LocationLookUpError: + await ctx.send('{}, you do not have a location called **{}**.'.format( + ctx.message.author.mention, loc)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def edit_tunnel(self, ctx, tunnel_direction: str, tunnel_number: int, *args): + """ + Edits the tunnel of a location + + Directions: North South East West + ?edit_tunnel [Tunnel Direction] [Tunnel Number] [Location Name] + """ + loc = get_name(args) + try: + loc_str = self.bot.bot_commands.edit_tunnel(tunnel_direction, tunnel_number, loc, + discord_uuid=ctx.message.author.id) + + await ctx.send( + '{}, the following location has been updated: \n\n{}'.format(ctx.message.author.mention, loc_str)) + except LocationLookUpError: + await ctx.send('{}, you do not have a location called **{}**.'.format( + ctx.message.author.mention, loc)) + except InvalidTunnelError: + await ctx.send( + '{}, **{}** is an invalid tunnel direction.'.format(ctx.message.author.mention, tunnel_direction)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def edit_name(self, ctx, new_name: str, current_name: str): + """ + Edits the name of a location + + IF A NAME HAS SPACES IN IT YOU NEED TO WRAP IT IN QUOTATION MARKS. eg. "Cool Shop 123" + ?edit_name [New Name] [Current Name] + """ + try: + loc_str = self.bot.bot_commands.edit_name(new_name, current_name, discord_uuid=ctx.message.author.id) + + await ctx.send( + '{}, the following location has been updated: \n\n{}'.format(ctx.message.author.mention, loc_str)) + except LocationLookUpError: + await ctx.send('{}, you do not have a location called **{}**.'.format( + ctx.message.author.mention, current_name)) + + +def setup(bot): + bot.add_cog(Edit_Commands(bot)) diff --git a/api/bot/cogs/Search_Commands.py b/api/bot/cogs/Search_Commands.py new file mode 100644 index 0000000..ab1b0cb --- /dev/null +++ b/api/bot/cogs/Search_Commands.py @@ -0,0 +1,139 @@ +from discord.ext import commands + +from geoffrey.BotErrors import * +from geoffrey.DiscordHelperFunctions import * + + +class Search_Commands: + """ + Commands to find stuff. + """ + + def __init__(self, bot): + self.bot = bot + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def find(self, ctx, *args): + """ + Finds all the locations matching the search term + ?find [Search] + """ + search = get_name(args) + try: + + if search is None: + raise commands.UserInputError + + result = self.bot.bot_commands.find(search) + + await ctx.send( + '{}, The following entries match **{}**:\n{}'.format(ctx.message.author.mention, search, result)) + except LocationLookUpError: + await ctx.send( + '{}, no matches to **{}** were found in the database.'.format(ctx.message.author.mention, search)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def tunnel(self, ctx, player: str): + """ + Finds all the tunnels a player owns + ?tunnel [Player] + """ + try: + result = self.bot.bot_commands.tunnel(player) + + await ctx.send( + '{}, **{}** owns the following tunnel(s): \n{}'.format(ctx.message.author.mention, player, result)) + except LocationLookUpError: + await ctx.send('{}, no tunnels for **{}** were found in the database.' + .format(ctx.message.author.mention, player)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def find_around(self, ctx, x_pos: int, z_pos: int, *args): + """ + Finds all the locations around a certain point. + The radius defaults to 200 blocks if no value is given + + Default dimension is the overworld + ?find_around [X Coordinate] [Z Coordinate] [Radius] + """ + radius = 200 + dimension = 'Overworld' + + try: + if len(args) > 0: + radius = int(args[0]) + + base_string = self.bot.bot_commands.find_around(x_pos, z_pos, radius, dimension) + + if len(base_string) != 0: + await ctx.send('{}, the following locations(s) are within **{}** blocks of that point: \n {}'.format( + ctx.message.author.mention, radius, base_string)) + else: + await ctx.send('{}, there are no locations within {} blocks of that point' + .format(ctx.message.author.mention, radius)) + except ValueError: + await ctx.send( + '{}, invalid radius, the radius must be a whole number.'.format(ctx.message.author.mention, + radius)) + except InvalidDimError: + await ctx.send('{}, {} is an invalid dimension.'.format(ctx.message.author.mention, dimension)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def selling(self, ctx, *args): + """ + Lists all the shops selling an item + + ?selling [item] + """ + item_name = get_name(args) + + if item_name is None: + raise commands.UserInputError + try: + + result = self.bot.bot_commands.selling(item_name) + await ctx.send( + '{}, the following shop(s) sell **{}**: \n{}'.format(ctx.message.author.mention, item_name, result)) + except ItemNotFound: + await ctx.send('{}, no shop sells **{}**.'.format(ctx.message.author.mention, item_name)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def info(self, ctx, *args): + """ + Displays info about a location. + + If the location is a shop, it displays the shop's inventory + ?info [Location Name] + """ + loc = get_name(args) + try: + + if loc is None: + raise commands.UserInputError + + info_str = self.bot.bot_commands.info(loc) + await ctx.send(info_str) + except LocationLookUpError: + await ctx.send('{}, no locations in the database match **{}**.'.format(ctx.message.author.mention, loc)) + + @commands.command(pass_context=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def me(self, ctx): + """ + Displays all your locations in the database + """ + try: + loc_str = self.bot.bot_commands.me(discord_uuid=ctx.message.author.id) + await ctx.send('{}, here are your location(s) in the database: \n {}'.format(ctx.message.author.mention, + loc_str)) + except PlayerNotFound: + await ctx.send('{}, you don\'t have any locations in the database.'.format(ctx.message.author.mention)) + + +def setup(bot): + bot.add_cog(Search_Commands(bot)) diff --git a/api/bot/cogs/__init__.py b/api/bot/cogs/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/bot/cogs/__init__.py @@ -0,0 +1 @@ + diff --git a/api/commands.py b/api/commands.py new file mode 100644 index 0000000..cde7c2e --- /dev/null +++ b/api/commands.py @@ -0,0 +1,228 @@ +from GeoffreyApp.api.bot.BotErrors import * +from django.db.models import Q, F +from GeoffreyApp.models import * +from GeoffreyApp.api.bot.MinecraftAccountInfoGrabber import * + +post_list = [] +get_list = [] +delete_list = [] + + +def post(func): + def command(): + post_list.append(func) + + return command() + + +def delete(func): + def command(): + delete_list.append(func) + + return command() + + +def get(func): + def command(): + get_list.append(func) + + return command() + + +def get_player(discord_uuid=None, mc_uuid=None): + if discord_uuid is not None: + player = Player.objects.get(discord_uuid=discord_uuid) + elif mc_uuid is not None: + player = Player.objects.get(mc_uuid=discord_uuid) + else: + raise AttributeError + + return player + + +def get_location(owner, name=None, loc_type=Location): + if name is None: + loc_list = Location.objects.all().select_related(loc_type.__name__).get(owner=owner) + if len(loc_list) == 1: + loc = loc_list[0] + elif len(loc_list) == 0: + raise NoLocationsInDatabase + else: + raise EntryNameNotUniqueError + else: + loc_list = Location.objects.all().select_related(loc_type.__name__).get(owner=owner, name=name) + if len(loc_list) == 1: + loc = loc_list[0] + else: + raise LocationLookUpError + + return loc + + +@post +def register(player_name, discord_uuid): + mc_uuid = grab_UUID(player_name) + player = Player.objects.create(name=player_name, mc_uuid=mc_uuid, discord_uuid=discord_uuid) + + return player.json + + +@post +def add_location(x_pos, z_pos, name=None, discord_uuid=None, mc_uuid=None, loc_type=Location): + player = get_player(discord_uuid, mc_uuid) + try: + get_location(player, name, loc_type=loc_type) + raise EntryNameNotUniqueError + except (NoLocationsInDatabase, LocationLookUpError): + if name is None: + name = "{}'s {}".format(player.name, loc_type.__name__) + + if loc_type == Base: + location = Base.objects.create(owner=player, name=name, x_coord=x_pos, z_coord=z_pos) + elif loc_type == Shop: + location = Shop.objects.create(owner=player, name=name, x_coord=x_pos, z_coord=z_pos) + else: + raise DataBaseError + + return location + + +@post +def add_tunnel(tunnel_direction, tunnel_number, location_name=None, discord_uuid=None, mc_uuid=None): + player = get_player(discord_uuid, mc_uuid) + if location_name is None: + loc = get_location(player, name=location_name) + location_name = loc.name + + tunnel = Tunnel.objects.create(tunnel_direction, tunnel_number, Location=get_location(player, location_name)) + + return tunnel + + +@get +def find_location(search): + limit = 25 + + locations = Location.objects.filter(Q(name__icontains=search) | Q(owner__name__icontains=search))[:limit] + + if len(locations) == 0: + raise LocationLookUpError + + return locations + + +@delete +def delete(name, discord_uuid=None, mc_uuid=None): + owner = get_player(discord_uuid, mc_uuid) + Location.objects.get(name__iexact=name, owner=owner) + + +@get +def find_around(x_pos, z_pos, radius=200): + locations = Location.objects.get(x_coord__range=(x_pos - radius, x_pos + radius), + z_coord__range=(z_pos - radius, z_pos + radius), dimension='O') + + return locations + + +@post +def add_item(item_name, quantity, diamond_price, shop_name=None, discord_uuid=None, mc_uuid=None): + player = get_player(discord_uuid, mc_uuid) + + shop = get_location(player, shop_name, Shop) + + item_listing = ItemListing.objects.create(shop=shop, quantity=quantity, price=diamond_price, item_name=item_name) + + return item_listing + + +# TODO Re-implement selling shop search +@get +def selling(item_name): + if len(item_name) == 0: + raise EmptryString + + items = ItemListing.objects.annotate(normalized_price=F('price') / F('amount')).filter(item_name__icontains=item_name).order_by('normalized_price') + + if len(items) == 0: + raise ItemNotFound + + return items + + +@get +def info(location_name): + loc = Location.objects.get(name__iexact=location_name) + return loc + + +@get +def tunnel(player_name): + tunnels = Tunnel.objects.get(location__owner__name__icontains=player_name) + + if len(tunnels) == 0: + raise LocationLookUpError + + return tunnel + + +@post +def edit_pos(x, z, loc_name, discord_uuid=None, mc_uuid=None): + + player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) + location = get_location(player, loc_name) + + location.x = x + location.z = z + location.save() + + return location + + +@post +def edit_tunnel(tunnel_direction, tunnel_number, loc_name, discord_uuid=None, mc_uuid=None): + player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) + location = get_location(player, loc_name) + + if location.tunnel is not None: + location.tunnel.tunnel_direction = tunnel_direction + location.tunnel.tunnel_number = tunnel_number + else: + Tunnel.objects.create(tunnel_direction=tunnel_direction, tunnel_number=tunnel_number, location=location) + + return location + + +@post +def edit_name(new_name, loc_name, discord_uuid=None, mc_uuid=None): + player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) + location = get_location(player, loc_name) + + location.name = new_name + location.save() + + return location + + +@post +def delete_item(item, shop_name, discord_uuid=None, mc_uuid=None): + player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) + + shop = get_location(player, shop_name, Shop) + + ItemListing.objects.filter(item_name__iexact=item, shop=shop).delete() + + return shop + + +@get +def me(discord_uuid=None, mc_uuid=None): + player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) + + locations = Location.objects.get(owner=player) + + if len(locations) == 0: + raise PlayerNotFound + + return locations + diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..cd7ce8c --- /dev/null +++ b/api/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from django.views.decorators.csrf import csrf_exempt +import GeoffreyApp.api.views as api + +urlpatterns = [ + url(r'^command/(?P\w{1,20})/$', csrf_exempt(api.CommandAPI.as_view()), name="api-command"), +] diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..bf040e8 --- /dev/null +++ b/api/views.py @@ -0,0 +1,36 @@ +from django.views.generic import View +from django.http import JsonResponse + +import GeoffreyApp.api.commands as commands +from GeoffreyApp.api.bot.BotErrors import * + + +def run_command(request, command, command_list): + command = command.lower() + response = [] + try: + for c in command_list: + if command == c.__name__: + ret = c(**request.dict()) + response.append(ret) + return JsonResponse(response, safe=False) + + raise CommandNotFound + except Exception as e: + response.append({"error": e.__class__.__name__}) + + return JsonResponse(response, safe=False) + + +class CommandAPI(View): + def get(self, request, command): + get = request.GET + return run_command(get, command, commands.get_list) + + def post(self, request, command): + post = request.POST + return run_command(post, command, commands.post_list) + + def delete(self, request, command): + delete = request.DELETE + return run_command(delete, command, commands.delete_list) \ No newline at end of file diff --git a/apps.py b/apps.py deleted file mode 100644 index 4af1988..0000000 --- a/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class GeoffreyConfig(AppConfig): - name = 'geoffrey' diff --git a/assets/bots/geoffrey.py b/assets/bots/geoffrey.py new file mode 100755 index 0000000..44d1baa --- /dev/null +++ b/assets/bots/geoffrey.py @@ -0,0 +1,14 @@ +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Geoffrey.settings") + +if __name__ == '__main__': + import django + django.setup() + from GeoffreyApp.api.bot.bot import start_bot + + start_bot() + + + + diff --git a/migrations/0001_initial.py b/migrations/0001_initial.py index d714d08..79d23ab 100644 --- a/migrations/0001_initial.py +++ b/migrations/0001_initial.py @@ -51,30 +51,30 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Base', fields=[ - ('location_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='geoffrey.Location')), + ('location_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='GeoffreyApp.Location')), ], - bases=('geoffrey.location',), + bases=('GeoffreyApp.location',), ), migrations.CreateModel( name='Shop', fields=[ - ('location_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='geoffrey.Location')), + ('location_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='GeoffreyApp.Location')), ], - bases=('geoffrey.location',), + bases=('GeoffreyApp.location',), ), migrations.AddField( model_name='tunnel', name='location', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tunnel_location', to='geoffrey.Location'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tunnel_location', to='GeoffreyApp.Location'), ), migrations.AddField( model_name='location', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner_player', to='geoffrey.Player'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner_player', to='GeoffreyApp.Player'), ), migrations.AddField( model_name='itemlisting', name='shop', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shop_selling', to='geoffrey.Shop'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shop_selling', to='GeoffreyApp.Shop'), ), ] diff --git a/models.py b/models.py index 7a62bf4..f53be05 100644 --- a/models.py +++ b/models.py @@ -1,13 +1,17 @@ from django.db import models from django.conf import settings +from sys import maxsize # Create your models here. - class Player(models.Model): - name = models.CharField(max_length=30) - mc_uuid = models.CharField(max_length=36) - discord_uuid = models.CharField(max_length=50) + name = models.CharField(max_length=30, unique=True) + mc_uuid = models.CharField(max_length=36, unique=True) + discord_uuid = models.CharField(max_length=50, unique=True) + + @property + def json(self): + return {"Name": self.name, "MC UUID": self.mc_uuid, "Discord UUID": self.discord_uuid} class Location(models.Model): @@ -25,6 +29,11 @@ class Location(models.Model): owner = models.ForeignKey(Player, related_name='owner_player', on_delete=models.CASCADE) + @property + def json(self): + return {"Type": str(type(self)), "Name": self.name, "x_coord": self.x_coord, "z_coord": self.z_coord, + "dimension": self.dimension, "Owner": self.owner.json} + class Shop(Location): def __str__(self): @@ -43,6 +52,13 @@ class ItemListing(models.Model): shop = models.ForeignKey(Shop, related_name="shop_selling", on_delete=models.CASCADE) + @property + def normalized_price(self): + if self.amount == 0: + return maxsize + else: + return self.price/self.amount + def __str__(self): return "Item: %d %s for %d" % (self.amount, self.item_name, self.amount) diff --git a/views.py b/views.py index 47865be..4058397 100644 --- a/views.py +++ b/views.py @@ -1,6 +1,9 @@ from django.shortcuts import render from django.http import HttpResponse +import subprocess, os # Create your views here. + + def index(request): return HttpResponse("Geoffrey is here!")