from django.db.models import Q, F from GeoffreyApp.models import * from GeoffreyApp.errors import * from GeoffreyApp.minecraft_api import * import inspect import datetime import re import enum from GeoffreyApp.util import objects_list_to_json class RequestTypes(enum.Enum): POST = "POST" GET = "GET" command_dict = {RequestTypes.GET: {}, RequestTypes.POST: {}} class Command: def __init__(self, func, permission_level, command_name=None): self.func = func self.params = self.get_required_args(func) self.help = self.parse_help(func) self.permission_level = permission_level if command_name is None: self.name = func.__name__ else: self.name = command_name @staticmethod def parse_help(func): try: match = re.search(".*:help:.*", func.__doc__) return match.group(0).partition(":help: ")[-1] except: return ' ' @staticmethod def get_required_args(func): args = inspect.getfullargspec(func) return args.args + args.kwonlyargs @property def json(self): return {"command": self.name, "help": self.help, "permission_level": self.permission_level.name} def command(type, permission_level=PermissionLevel.PLAYER, command_name=None): def command_dec(func): def add_command(): command_obj = Command(func, permission_level, command_name=command_name) command_dict[type][command_obj.name] = command_obj return func return add_command() return command_dec def match_tunnel(tunnel_direction): for direction in Tunnel.TUNNEL_NAMES: if re.search("{}.*".format(tunnel_direction), direction[1], re.IGNORECASE): return direction[0] raise InvalidTunnelError def get_player_by_name(mc_username): try: uuid = grab_UUID(mc_username) return get_player(mc_uuid=uuid) except UsernameLookupFailed: raise PlayerNotFound def get_player(discord_uuid=None, mc_uuid=None): try: discord_uuid = str(discord_uuid) if discord_uuid else None mc_uuid = str(mc_uuid) if mc_uuid else None if discord_uuid is not None: player = Player.objects.get(discord_uuid__iexact=discord_uuid) elif mc_uuid is not None: player = Player.objects.get(mc_uuid__iexact=mc_uuid) else: raise AttributeError except Player.DoesNotExist: raise PlayerNotFound return player def get_location(owner, name=None, loc_type=Location): if name is None: loc_list = loc_type.objects.filter(owner=owner).all() if len(loc_list) == 1: loc = loc_list[0] elif len(loc_list) == 0: raise NoLocationsInDatabase else: raise EntryNameNotUniqueError else: loc_list = loc_type.objects.filter(owner=owner, name__iexact=name).all() if len(loc_list) == 1: loc = loc_list[0] else: raise LocationLookUpError return loc def add_location(x_pos, z_pos, dimension, name=None, discord_uuid=None, mc_uuid=None, loc_type=Location): player = get_player(discord_uuid, mc_uuid) name_was_none = False if name is None: name = "{}'s {}".format(player.name, loc_type.__name__) name_was_none = True if Location.objects.filter(name__iexact=name).all().count() > 0: if name_was_none: raise LocationLookUpError else: raise EntryNameNotUniqueError else: location = loc_type.objects.create(name=name, x_coord=x_pos, z_coord=z_pos, dimension=dimension) location.owner.add(player) location.save() return location.json @command(RequestTypes.POST) def register(player_name, discord_uuid=None): """ :request: POST :param player_name: Minecraft in-game name :param discord_uuid: Discord UUID if registering from Discord :return: JSON representation of the new Player :raise: PlayerInDBError :help: Registers your Discord and Minecraft account with the the database """ mc_uuid = grab_UUID(player_name) try: player = get_player(mc_uuid=mc_uuid) if discord_uuid is not None and player.discord_uuid is None: player.discord_uuid = discord_uuid player.save() return player.json else: raise PlayerInDBError except PlayerNotFound: player = Player.objects.create(name=player_name, mc_uuid=mc_uuid, discord_uuid=discord_uuid) player.save() return player.json @command(RequestTypes.POST) def add_base(x_pos, z_pos, dimension="O", name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param dimension: MC Dimension :param name: Base Name (If None, Defaults to Player's Base) :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON representation of the new base :raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError :help: Adds your base to the database. """ return add_location(x_pos, z_pos, dimension, name=name, discord_uuid=discord_uuid, mc_uuid=mc_uuid, loc_type=Base) @command(RequestTypes.POST) def add_shop(x_pos, z_pos, dimension="O", name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param dimension: MC Dimension :param name: Shop Name (If None, Defaults to Player's Shop) :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON representation of the new shop :raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError :help: Adds your shop to the database. """ return add_location(x_pos, z_pos, dimension, name=name, discord_uuid=discord_uuid, mc_uuid=mc_uuid, loc_type=Shop) @command(RequestTypes.POST) def add_town(x_pos, z_pos, dimension="O", name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param dimension: MC Dimension :param name: Town Name (If None, Defaults to Player's Town) :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON representation of the new town :raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError :help: Adds your town to the database. """ return add_location(x_pos, z_pos, dimension, name=name, discord_uuid=discord_uuid, mc_uuid=mc_uuid, loc_type=Town) @command(RequestTypes.POST) def add_farm(x_pos, z_pos, dimension="O", name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param dimension: MC Dimension :param name: Farm Name (If None, Defaults to Player's Farm) :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON representation of the new farm :raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError :help: Adds your public farm to the database. """ return add_location(x_pos, z_pos, dimension, name=name, discord_uuid=discord_uuid, mc_uuid=mc_uuid, loc_type=PublicFarm) @command(RequestTypes.POST) def add_market(x_pos, z_pos, dimension="O", name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param dimension: MC Dimension :param name: Market Name (If None, Defaults to Player's Market) :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON representation of the market :raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError :help: Adds your market to the database. """ return add_location(x_pos, z_pos, dimension, name=name, discord_uuid=discord_uuid, mc_uuid=mc_uuid, loc_type=Market) @command(RequestTypes.POST) def add_attraction(x_pos, z_pos, dimension="O", name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param dimension: MC Dimension :param name: Market Name (If None, Defaults to Player's Attraction) :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON representation of the attraction :raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError :help: Adds your attraction to the database. """ return add_location(x_pos, z_pos, dimension, name=name, discord_uuid=discord_uuid, mc_uuid=mc_uuid, loc_type=Attraction) @command(RequestTypes.POST) def add_tunnel(tunnel_direction, tunnel_number, location_name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param tunnel_direction: Tunnel Direction :param tunnel_number: Tunnel Coordinate :param location_name: Name of the Location to attach the tunnel to :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: JSON Representation of the Tunnel :raises: PlayerNotFound, LocationHasTunnelError, EntryNameNotUniqueError, NoLocationsInDatabase, InvalidTunnelError :help: Adds your tunnel to the database. """ player = get_player(discord_uuid, mc_uuid) if location_name is None: loc = get_location(player) location_name = loc.name else: loc = get_location(player, name=location_name) tunnel_direction = match_tunnel(tunnel_direction) if Tunnel.objects.filter(location=loc).first(): raise LocationHasTunnelError tunnel = Tunnel.objects.create(tunnel_direction=tunnel_direction, tunnel_number=tunnel_number, location=get_location(player, location_name)) return tunnel.json @command(RequestTypes.GET) def find_location(search, limit=25): """ :request: GET :param search: Location name or owner to search for :param limit: How man locations to return :return: List of the matching locations :raises: LocationLookupError :help: Finds all the locations matching the search term """ locations = Location.objects.filter(Q(name__icontains=search) | Q(owner__name__icontains=search)).all()[:limit] if len(locations) == 0: raise LocationLookUpError return objects_list_to_json(locations) @command(RequestTypes.POST) def delete(name, discord_uuid=None, mc_uuid=None): """ :request: POST :param name: Name of location to delete :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: Location Name :raises: LocationLookUpError :help: Deletes a location from the database """ owner = get_player(discord_uuid, mc_uuid) try: if Location.objects.get(name__iexact=name, owner=owner).delete() == 0: raise Location.DoesNotExist except Location.DoesNotExist: raise LocationLookUpError return name @command(RequestTypes.GET) def find_around(x_pos, z_pos, radius=200): """ :request: GET :param x_pos: MC X Coordinate :param z_pos: MC Z Coordinate :param radius: Radius to each around (default 200) :return: List of all locations in the radius :raises: LocationLookupError :help: Finds all the locations around a certain point. """ x_pos = int(x_pos) z_pos = int(z_pos) radius = int(radius) locations = Location.objects.filter(x_coord__range=(x_pos - radius, x_pos + radius), z_coord__range=(z_pos - radius, z_pos + radius)).all() if len(locations) == 0: raise LocationLookUpError return objects_list_to_json(locations) @command(RequestTypes.POST) def add_item(item_name, quantity, diamond_price, shop_name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param item_name: name of the item :param quantity: number of items being sold :param diamond_price: price in diamonds :param shop_name: Name of the shop, can be blank if the user only has one shop :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: Item Listing :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase :help: Adds an item to a shop's inventory. """ player = get_player(discord_uuid, mc_uuid) shop = get_location(player, shop_name, Shop).shop item_listing = ItemListing.objects.create(shop=shop, amount=int(quantity), price=int(diamond_price), item_name=item_name) return item_listing.json @command(RequestTypes.POST) def add_resource(resource_name, farm_name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param resource_name: name of the resource :param farm_name: name of the farm to add the resource to. Can be none. :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: Item Listing :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase :help: Adds a resource to a farm. """ player = get_player(discord_uuid, mc_uuid) farm = get_location(player, farm_name, PublicFarm).publicfarm resource = Resource.objects.create(farm=farm, resource_name=resource_name) return resource.json @command(RequestTypes.GET) def find_farm(resource_name): """ :request: GET :param resource_name: Resource to search for :return: List of top matching farms :raises: ResourceNotFoundError :help: Lists farms that produce a resource. """ if len(resource_name) == 0: raise EmptryString farms = PublicFarm.objects.filter(resource__resource_name__icontains=resource_name).all()[:10] if len(farms) == 0: raise ResourceNotFoundError return objects_list_to_json(farms) @command(RequestTypes.GET) def selling(item_name): """ :request: GET :param item_name: Item name to search for :return: List of top matching shops, sorted by the date last restocke :raises: ItemNotFound :help: Lists shops selling an item. Sorted by when they were last restocked. """ return get_selling(item_name, sort="-date_restocked") @command(RequestTypes.GET) def selling_price(item_name): """ :request: GET :param item_name: Item name to search for :return: List of top matching shops, sorted by the :raises: ItemNotFound :help: Lists shops selling an item. Sorted lowest price to highest price. """ return get_selling(item_name, sort="normalized_price") def get_selling(item_name, sort): items = [] if len(item_name) == 0: raise EmptryString all_shop_ids = ItemListing.objects.filter(item_name__icontains=item_name).annotate(normalized_price=F('price') / F('amount')).order_by(sort).values("shop_id").all() if all_shop_ids.count() == 0: raise ItemNotFound # Removes duplicates shop_ids = [] for shop_id in all_shop_ids: if shop_id not in shop_ids: shop_ids.append(shop_id) for shop_id in shop_ids: shop = Shop.objects.get(pk=shop_id['shop_id']).json item_query = ItemListing.objects.filter( item_name__icontains=item_name, shop_id=shop_id['shop_id']).order_by("-date_restocked") item_list = [] for item in item_query: item_list.append(item) shop["items"] = objects_list_to_json(item_list) items.append(shop) return items @command(RequestTypes.GET) def info(location_name): """ :request: GET :param location_name: Name of the location to get info on. :return: JSON representation of location :raises: LocationLookupError :help: Gives additional info about a location. """ location = Location.objects.filter(name__iexact=location_name).first() if location is None: location = Location.objects.filter(name__iregex=".*{}.*".format(location_name)).first() if location is not None: loc = location.loc_child_obj loc_json = loc.json if type(loc) == Shop: loc_json["items"] = [] for item in loc.shop_selling.all(): loc_json["items"].append(item.json) elif type(loc) == PublicFarm: loc_json["resources"] = [] for resource in loc.resource.all(): loc_json["resources"].append(resource.json) return loc_json else: raise LocationLookUpError @command(RequestTypes.GET) def tunnel(player_name): """ :request: GET :param player_name: MC player name :return: List of all the tunnels a user owns :raises: LocationLookUpError :raises: LocationLookupError :help: Finds all the tunnels a player owns. """ tunnels = Tunnel.objects.filter(location__owner__name__icontains=player_name).all() if len(tunnels) == 0: raise LocationLookUpError return objects_list_to_json(tunnels) @command(RequestTypes.POST) def edit_pos(x, z, loc_name, dimension="O", discord_uuid=None, mc_uuid=None): """ :request: POST :param x: New MC X coordinate :param z: New MC Z Coordinate :param dimension: MC Dimension :param loc_name: Location Name to edit :param discord_uuid: Discord UUID :param mc_uuid: MC UUID :return: Edited Location :raises: :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase :help: Edits the position of a location. """ player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) location = get_location(player, loc_name) location.x_coord = x location.z_coord = z location.dimension = dimension location.save() return location.json @command(RequestTypes.POST) def edit_tunnel(tunnel_direction, tunnel_number, loc_name, discord_uuid=None, mc_uuid=None): """ :request: POST :param tunnel_direction: New Tunnel Direction :param tunnel_number: New Tunnel Address :param loc_name: Location Name to edit :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: Edited Location :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase :help: Edits the tunnel of a location. """ player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) location = get_location(player, loc_name) tunnel_direction = match_tunnel(tunnel_direction) try: tunnel = Tunnel.objects.get(location=location) tunnel.tunnel_direction = tunnel_direction tunnel.tunnel_number = tunnel_number tunnel.save() except Tunnel.DoesNotExist: Tunnel.objects.create(tunnel_direction=tunnel_direction, tunnel_number=tunnel_number, location=location) return location.json @command(RequestTypes.POST) def edit_name(new_name, loc_name, discord_uuid=None, mc_uuid=None): """ :request: POST :param new_name: New Location Name :param loc_name: Old Location name :param discord_uuid: Discord UUID :param mc_uuid: MC UUID :return: Edited Location :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase :help: Edits the name of a location. """ player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) if Location.objects.filter(name__iexact=new_name).count(): raise EntryNameNotUniqueError location = get_location(player, loc_name) location.name = new_name location.save() return location.json @command(RequestTypes.POST) def delete_item(item, shop_name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param item: Item name to delete :param shop_name: Shop selling item, can be None if the user only has one shop :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: Shop where the item was deleted from :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase, ItemNotFound :help: Deletes an item from a shop. """ player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) shop = get_location(player, shop_name, Shop) delete_list = ItemListing.objects.filter(item_name__iexact=item, shop=shop).all() if len(delete_list) == 0: raise ItemNotFound delete_list.delete() return shop.json @command(RequestTypes.POST) def delete_resource(resource, farm_name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param resource: resource to delete :param farm_name: Farm with resource, can be None if the user only has one farm :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: PublicFarm where the resource was deleted from :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase, ItemNotFound :help: Deletes a resource from a farm. """ player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) farm = get_location(player, farm_name, PublicFarm) delete_list = Resource.objects.filter(resource_name=resource, farm=farm).all() if len(delete_list) == 0: raise ResourceNotFoundError delete_list.delete() return farm.json @command(RequestTypes.GET) def me(discord_uuid=None, mc_uuid=None): """ :request: GET :param discord_uuid: Discord UUID :param mc_uuid: MC UUID :return: Returns a list of all the locations owned by a user :raises: NoLocationsInDatabase, PlayerNotFound :help: Find all the locations owned by a player in the database. """ try: player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid) except Player.DoesNotExist: raise PlayerNotFound locations = Location.objects.filter(owner=player).all() if len(locations) == 0: raise NoLocationsInDatabase return objects_list_to_json(locations) @command(RequestTypes.POST) def restock(item_name, shop_name=None, discord_uuid=None, mc_uuid=None): """ :request: POST :param item_name: Item to restock :param shop_name: Shop the item is in, can be none if the only one location is owned by the user :param discord_uuid: Discord UUID :param mc_uuid: Minecraft UUID :return: List of items updated :raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase, ItemNotFound :help: Restocks items matching the item name in your shop. """ owner = get_player(discord_uuid, mc_uuid) shop = get_location(owner, shop_name, Shop) items = ItemListing.objects.filter(item_name__iregex=item_name, shop=shop).all() if len(items) == 0: raise ItemNotFound else: for item in items: item.date_restocked = datetime.datetime.now() item.save() return objects_list_to_json(items) @command(RequestTypes.POST) def add_owner(new_owner_name, location_name, discord_uuid=None, mc_uuid=None): """ :request: POST :param new_owner_name: The MC username of the new owner :param location_name: The name of the location to add them to :param discord_uuid: Discord UUID of the current owner :param mc_uuid: MC UUID of the current owner :return: Update Location :raises: PlayerNotFound, LocationLookupError, IsOwnerError, OwnerNotFound :help: Adds a co-owner to a location. """ owner = get_player(discord_uuid, mc_uuid) try: new_owner = Player.objects.get(name__iexact=new_owner_name) except Player.DoesNotExist: raise OwnerNotFoundError location = get_location(owner, location_name, Location) if location.owner.filter(id=new_owner.id): raise IsOwnerError location.owner.add(new_owner) location.save() return location.json @command(RequestTypes.POST) def add_resident(new_resident_name, town_name, discord_uuid=None, mc_uuid=None): """ :request: POST :param new_resident_name: The MC username of the new resident :param town_name: The name of the town to add the resident to, can be blank if the owner has one town :param discord_uuid: Discord UUID of the town owner :param mc_uuid: MC UUID of the town owner :return: Updated Location :raises: PlayerNotFound, LocationLookupError, IsResidentError, ResidentNotFoundError :help: Adds a resident to a town. """ owner = get_player(discord_uuid, mc_uuid) try: new_resident = Player.objects.get(name__iexact=new_resident_name) except Player.DoesNotExist: raise ResidentNotFoundError town = get_location(owner, town_name, Town) if town.residents.filter(id=new_resident.id).exists() | town.owner.filter(id=new_resident.id).exists(): raise IsResidentError town.residents.add(new_resident) town.save() return town.json @command(RequestTypes.POST) def remove_resident(resident_name, town_name, discord_uuid=None, mc_uuid=None): """ :request: POST :param resident_name: Name of the resident to remove :param town_name: Name of the town, can be blank if the owner has one town :param discord_uuid: Owner discord uuid :param mc_uuid: Owner mc uuid :raises: PlayerNotFound, LocationLookupError, ResidentNotFoundError :return: Updated town :help: Removes a resident from a town. """ owner = get_player(discord_uuid, mc_uuid) town = get_location(owner, town_name, Town) try: resident = town.residents.get(name__iexact=resident_name) except Player.DoesNotExist: raise ResidentNotFoundError town.residents.remove(resident) town.save() return town.json @command(RequestTypes.POST, permission_level=PermissionLevel.MOD) def mod_delete(player_name, location_name): uuid = grab_UUID(player_name) delete(location_name, mc_uuid=uuid) @command(RequestTypes.POST, permission_level=PermissionLevel.MOD) def mod_delete_item(player_name, item_name, shop_name): uuid = grab_UUID(player_name) delete_item(item_name, shop_name=shop_name, mc_uuid=uuid) @command(RequestTypes.POST, permission_level=PermissionLevel.MOD) def mod_edit_name(player_name, new_name, loc_name): uuid = grab_UUID(player_name) edit_name(new_name, loc_name, mc_uuid=uuid) @command(RequestTypes.POST, permission_level=PermissionLevel.MOD) def mod_remove_resident(player_name, resident_name, town_name): uuid = grab_UUID(player_name) remove_resident(resident_name, town_name, mc_uuid=uuid) @command(RequestTypes.POST, permission_level=PermissionLevel.MOD) def mod_remove_owner(player_name, loc_name): uuid = grab_UUID(player_name) owner = get_player(mc_uuid=uuid) location = get_location(owner, loc_name) location.owner.remove(owner) @command(RequestTypes.POST, permission_level=PermissionLevel.PLAYER) def primary_location(loc_name, discord_uuid=None, mc_uuid=None): """ :request: POST :param loc_name: location to set as primary :param discord_uuid: player discord uuid :param mc_uuid: player mc uuid :return: json representation of the primary location """ player = get_player(discord_uuid, mc_uuid) loc = get_location(player, loc_name) player.primary_location = loc player.save() return loc.json