├── .gitignore ├── LICENSE ├── README.md ├── api ├── api_dev.py ├── api_tests.py ├── base_models.py ├── data │ ├── calculators │ │ ├── base_item_calculator.py │ │ ├── dungeon_calculator.py │ │ ├── enchantment_calculator.py │ │ ├── main_calculator_handler.py │ │ └── pet_calculator.py │ ├── constants │ │ ├── bazaar.py │ │ ├── collector.py │ │ ├── enchantment_levels.py │ │ ├── enchants.py │ │ ├── enchants_top.py │ │ ├── essence.py │ │ ├── jerry_price_list.py │ │ ├── lowest_bin.py │ │ ├── npc_items.py │ │ ├── pets.py │ │ └── reforges.py │ ├── container_handler.py │ ├── decode_container.py │ ├── item_object.py │ └── price_object.py ├── endpoints │ ├── dump.py │ ├── groups.py │ ├── pages.py │ ├── total.py │ └── tree.py ├── exceptions.py ├── favicon.ico ├── main.py ├── requirements.txt └── zz check for.py ├── bot ├── bot.py ├── database_manager.py ├── dev.py ├── emojis.py ├── extract_ids.py ├── menus.py ├── networth │ ├── constants.py │ ├── generate_description.py │ ├── generate_page.py │ ├── guild_networth.py │ └── networth.py ├── parse_profile.py ├── player_commands │ ├── __init__.py │ ├── auction_house.py │ ├── bazaar.py │ ├── dungeons.py │ ├── duped.py │ ├── guild_print.py │ ├── help_command.py │ ├── invite.py │ ├── kills.py │ ├── leaderboard.py │ ├── link_account.py │ ├── lowest_bin.py │ ├── maxer.py │ ├── minions - Copy.py │ ├── minions.py │ ├── missing.py │ ├── notify.py │ ├── populate_leaderboard.py │ ├── price_check.py │ ├── rank.py │ ├── regenerate_leaderboard.py │ ├── set_prefix.py │ ├── skills.py │ ├── sky.py │ ├── slayer.py │ ├── weights.py │ └── wiki.py ├── requirements.txt ├── test_cog.py ├── text_files │ ├── MASTER_ITEM_DICT.json │ ├── accessory_list.py │ └── uuid_conversion_cache.json ├── utils.py └── zz check for.py └── info ├── how_things_are_calculated.md ├── privacy_policy.md ├── simple_example.js ├── simple_example.py └── terms_of_service.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | dead_code 3 | .mypy_cache 4 | bot/text_files/database_creds.txt 5 | bot/text_files/bot_key.txt 6 | bot/text_files/dev_bot_key.txt 7 | bot/text_files/hypixel_api_key.txt 8 | bot/text_files/hypixel_api_key_weight.txt 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommunityBot · ![version](https://img.shields.io/badge/Version-2.1.0-brightgreen.svg?style=flat-square) ![license](https://img.shields.io/badge/License-MIT-brightgreen.svg?style=flat-square) [![Discord](https://img.shields.io/discord/571681282652766208.svg?style=flat-square&logo=discord&label=SkyblockCommunityServer&colorA=7289DA&colorB=2C2F33)](https://discord.gg/skyblockcommunity) 2 | 3 | # A Discord bot that is developed by [Skezza#1139](https://discord.com/users/244543752889303041) made to help the community in various things related to Hypixel Skyblock 4 | ### The bot has numerous commands, a highlight of them is as follows: 5 | - Checking the prices of items. 6 | - Checking a player's networth. 7 | - Checks if a player has obtained possibly duped items. 8 | - Checking a player's auction house and displays whether the item is sold or not. 9 | - Shows how to max a player's weapon/armour and guided them through the process. 10 | - Show a player's missing talismans, with the ability to sort the list **alphabetically** as well as by **rarity** and the price. 11 | - Show what is needed to max items in a player's inventory, mainly weapons. 12 | - Show the tiers of minions a player can craft to unlock more unqiue minion tiers, helping them unlock more minion slots. 13 | - And much more! 14 | 15 | ### Special thanks to: 16 | - Mega_Pi+Seattle72 for their research and knowledge on a lot of the content. 17 | - [egg#7777](https://discord.com/users/857950050915581983) for their insight as well as the laborous work. 18 | 19 | # A message to the community: 20 | This bot will remain open source, and provide fun information about your profile, such as it's value. 21 | Anyone can make a pull request, and it'll be reviewed and possibly added. 22 | Any future things that should be counted will generally be added quite quickly, as the system is very flexible. 23 | 24 | - For more information on how the API calculates its values, have a look in [how_things_are_calculated.md](info/how_things_are_calculated.md) 25 | - For an example of how to use the API, have a look at [simple_example.py](info/simple_example.py) or [simple_example.js](info/simple_example.js) 26 | 27 | # Useful links: 28 | Invite the bot: [Link](https://discord.com/api/oauth2/authorize?client_id=854722092037701643&permissions=242666032192&scope=bot%20applications.commands) 29 | 30 | Vote for the bot: [Link](https://discord.com/channels/854749884103917599/871727003060019300/892763258610675793) 31 | 32 | Join the support server: [Link](https://discord.gg/fhuPkpEYHX) 33 | 34 | Join our community server: [Link](https://discord.gg/skyblockcommunity) 35 | -------------------------------------------------------------------------------- /api/api_dev.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | test_usernames = {0: "56ms", 1: "nonbunary", 2: "poroknights", 5 | 3: "UrMinecraftDoggo", 4: "Skezza", 5: "kori_100", 6 | 6: "XD_Zaptro_XD", 7: "seattle72", 8: "Refraction"} 7 | 8 | user = 0 9 | username = test_usernames[user] 10 | 11 | # Skezza 12 | API_KEY = "78a53e82-85b5-442f-89cd-744285cbce80" 13 | 14 | 15 | #a = requests.get("https://api.hypixelskyblock.de/api/v1/cb/pages/balt") 16 | 17 | # Leaderboard players: 18 | #username = "NewEriwan" 19 | username = "Minikloon" 20 | #username = "Refraction" 21 | #username = "fela22" 22 | #username = "Makiso" 23 | username = "DeathStreeks" 24 | username = "Repurposer" 25 | username = "StutterMuch" 26 | username = "Ealman" 27 | username = "Skezza" 28 | #username = "oNicNoc" 29 | #username = "56ms" 30 | #username = "ycarusishere" 31 | #username = "KebabOnNaan" 32 | #username = "ItzAlpha__" 33 | #username = "balt" 34 | #username = "455fcc3f87ea4a92a6c38e190c39d8ec" 35 | #username = "seattle72" 36 | #username = "laachs" 37 | #username = "glai" 38 | #username = "lk" 39 | #username = "AndtheBand28" 40 | #username = "JasonHYH186" 41 | #username = "Jomis_" 42 | #username = "Everlasting_Luck" 43 | 44 | 45 | uuid = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{username}").json()["id"] 46 | 47 | #uuid = "1e82592262494e8fb814dffb7de916aa" 48 | #uuid = "0cb78dbf35e84a77bad937199a1a91ef" 49 | profile_data = requests.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}") 50 | if profile_data.status_code != 200: 51 | print(f"Key dead? Status code: {profile_data.status_code}") 52 | print(profile_data.text) 53 | 54 | profile_json = profile_data.json() 55 | 56 | ip = "127.0.0.1" # For running locally 57 | #ip = "panel.skyblockcommunity.com" 58 | #ip = "db.superbonecraft.dk" # For the server 59 | 60 | print("Before") 61 | a = requests.post(f"http://{ip}:8000/pages/{uuid}", json=profile_json) 62 | print("After") 63 | #a = requests.post(f"http://{ip}:8000/tree/{uuid}", json=profile_data) 64 | 65 | print(a.status_code) 66 | print(a.json()) 67 | -------------------------------------------------------------------------------- /api/api_tests.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import unittest 4 | from parameterized import parameterized, parameterized_class 5 | 6 | ip = "http://127.0.0.1:8000" # For running locally 7 | #ip = "http://db.superbonecraft.dk:8000" # For the server 8 | 9 | API_KEY = "" 10 | 11 | test_usernames = {0: "56ms", 1: "nonbunary", 2: "poroknights", 12 | 4: "Skezza", 5: "kori_100", 13 | 6: "Zaptro", 7: "seattle72", 14 | 8: "Refraction", 9: "laachs"} 15 | 16 | uuids = [requests.get(f"https://api.mojang.com/users/profiles/minecraft/{username}").json()["id"] for username in test_usernames.values()] 17 | 18 | DEFAULT_KEYS = ["profile_data", "purse", "banking", "inventory", "accessories", "ender_chest", "armor", "wardrobe", "vault", "storage", "pets"] 19 | 20 | def get_profile_data(uuid): 21 | profile_data = requests.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}").json() 22 | return profile_data 23 | 24 | @parameterized_class( 25 | ("uuid"), 26 | [(x,) for x in uuids], 27 | ) 28 | class TotalEndpoint(unittest.TestCase): 29 | 30 | print("Testing") 31 | 32 | #@unittest.skip("Skip") 33 | def test_a_total(self): 34 | profile_data = get_profile_data(self.uuid) 35 | r = requests.post(f"{ip}/total/{self.uuid}", json=profile_data) 36 | # Check of OK, 200 37 | self.assertEqual(r.status_code, 200) 38 | r = r.json() 39 | # Check that it's {"total": x} 40 | self.assertTrue(list(r.keys()) == ["total"]) 41 | # Check that it's only {"total": x} 42 | self.assertEqual(len(r.keys()), 1) 43 | # Check that the total isn't a string or None 44 | self.assertTrue(isinstance(r["total"], (float, int))) 45 | 46 | #@unittest.skip("Skip") 47 | def test_b_groups(self): 48 | profile_data = get_profile_data(self.uuid) 49 | r = requests.post(f"{ip}/groups/{self.uuid}", json=profile_data) 50 | # Check of OK, 200 51 | self.assertEqual(r.status_code, 200) 52 | r = r.json() 53 | # Check for right keys 54 | self.assertTrue(list(r.keys()) == DEFAULT_KEYS) 55 | # Check for wrong keys 56 | self.assertTrue(len(r.keys()) == len(DEFAULT_KEYS)) 57 | 58 | #@unittest.skip("Skip") 59 | def test_c_pages(self): 60 | profile_data = get_profile_data(self.uuid) 61 | r = requests.post(f"{ip}/pages/{self.uuid}", json=profile_data) 62 | # Check of OK, 200 63 | self.assertEqual(r.status_code, 200) 64 | r = r.json() 65 | # Check for right keys 66 | self.assertTrue(list(r.keys()) == DEFAULT_KEYS) 67 | # Check for wrong keys 68 | self.assertTrue(len(r.keys()) == len(DEFAULT_KEYS)) 69 | 70 | #@unittest.skip("Skip") 71 | def test_d_dump(self): 72 | profile_data = get_profile_data(self.uuid) 73 | r = requests.post(f"{ip}/dump/{self.uuid}", json=profile_data) 74 | # Check of OK, 200 75 | self.assertEqual(r.status_code, 200) 76 | r = r.json() 77 | # Check for right keys 78 | self.assertTrue(list(r.keys()) == DEFAULT_KEYS) 79 | # Check for wrong keys 80 | self.assertTrue(len(r.keys()) == len(DEFAULT_KEYS)) 81 | 82 | if __name__ == '__main__': 83 | print("Starting tests") 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /api/base_models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from pydantic.typing import Literal 3 | from enum import Enum 4 | from typing import Union 5 | 6 | default_response_types = { 7 | 401: {"description": "Invalid profile given. That player hasn't got a profile with that name."}, 8 | 402: {"description": "No profiles found for the given profile_data."}, 9 | 404: {"description": "UUID couldn't be found on that profile."}, 10 | 500: {"description": "An internal exception occured."}, 11 | } 12 | 13 | ################################################### 14 | 15 | class custom_body(BaseModel): 16 | success: str 17 | profiles: list[dict] 18 | 19 | class Item(BaseModel): 20 | total: int 21 | value: dict 22 | item: dict 23 | 24 | class Total(BaseModel): 25 | total: int 26 | 27 | class PriceGroup(BaseModel): 28 | total: int 29 | prices: list[Item] 30 | 31 | class ProfileData(BaseModel): 32 | profile_name: Literal['Apple | Banana | Blueberry | Coconut | Cucumber | Grapes | Kiwi | Lemon | Lime | Mango | Orange | Papaya | Pear | Peach | Pineapple | Pomegranate | Raspberry | Strawberry | Tomato | Watermelon | Zucchini'] 33 | profile_type: Literal['regular | ironman'] 34 | 35 | ################################################### 36 | class PagesOut(BaseModel): 37 | profile_data: ProfileData 38 | purse: Total 39 | banking: Total 40 | inventory: PriceGroup 41 | accessories: PriceGroup 42 | ender_chest: PriceGroup 43 | armor: PriceGroup 44 | wardrobe: PriceGroup 45 | vault: PriceGroup 46 | storage: PriceGroup 47 | pets: PriceGroup 48 | #========================== 49 | class TotalOut(BaseModel): 50 | total: int 51 | #========================== 52 | class GroupsOut(BaseModel): 53 | profile_data: ProfileData 54 | purse: int 55 | banking: int 56 | inventory: int 57 | accessories: int 58 | ender_chest: int 59 | armor: int 60 | wardrobe: int 61 | vault: int 62 | storage: int 63 | pets: int 64 | #========================== 65 | class DumpOut(BaseModel): 66 | pass 67 | #========================== 68 | class TreeOut(BaseModel): 69 | data: str 70 | #=========================== 71 | ''' 72 | with open("example_profile_data.txt", "r") as file: 73 | example_profile_data = file.read() 74 | 75 | pages_example_inputs = { 76 | "Pages for 56ms": { 77 | "summary": "Pages for 56ms", 78 | #"description": "A **normal** item works correctly.", 79 | "value": { 80 | "uuid": "1277d71f338046e298d90c9fe4055f00", 81 | "profile": "Strawberry", 82 | "body": example_profile_data, 83 | }, 84 | }, 85 | } 86 | ''' 87 | -------------------------------------------------------------------------------- /api/data/calculators/base_item_calculator.py: -------------------------------------------------------------------------------- 1 | from data.constants.reforges import REFORGE_DICT 2 | from data.calculators.dungeon_calculator import calculate_dungeon_item 3 | from data.calculators.enchantment_calculator import calculate_enchantments 4 | 5 | 6 | def calculate_reforge_price(data, price): 7 | item = price.item 8 | # This "+;item.item_group prevents warped for armor and AOTE breaking 9 | reforge_data = REFORGE_DICT.get(item.reforge+";"+item.item_group, None) 10 | # This will not calculate reforges that are from the blacksmith, e.g. "Wise", "Demonic", they're just not worth anything. 11 | if reforge_data is not None: 12 | reforge_item = reforge_data["INTERNAL_NAME"] # Gets the item, e.g. BLESSED_FRUIT 13 | item_rarity = item.rarity 14 | if item_rarity in ["SPECIAL", "VERY_SPECIAL"]: # The dataset doesn't include special, use LEGENDARY instead 15 | item_rarity = "LEGENDARY" 16 | #print("All data:", reforge_data, "\nInternal name:", item.internal_name) 17 | #print(reforge_data["REFORGE_COST"], item_rarity) 18 | reforge_cost = reforge_data["REFORGE_COST"].get(item_rarity, 0) # Cost to apply for each rarity 19 | 20 | # How much does the reforge stone cost, check bazaar 21 | reforge_item_cost = data.BAZAAR.get(reforge_item, 0) 22 | 23 | price.value["reforge"] = {} 24 | price.value["reforge"]["item"] = {reforge_item: reforge_item_cost} 25 | price.value["reforge"]["apply_cost"] = reforge_cost 26 | 27 | return price 28 | 29 | def is_npc_item(data, internal_name): 30 | for npc, items in data.NPC_ITEMS.items(): 31 | for item, item_price in items.items(): 32 | if item == internal_name: 33 | return item_price, npc 34 | return None, None 35 | 36 | def calculate_item(data, price, print_prices=False): 37 | 38 | item = price.item 39 | value = price.value 40 | 41 | # Calculate base price 42 | 43 | # If the item is in any of the npc's lists 44 | item_price, npc = is_npc_item(data, item.internal_name) 45 | if item_price is not None: 46 | if item.internal_name in data.LOWEST_BIN and data.LOWEST_BIN[item.internal_name] < item_price: 47 | value["base_price"] = data.LOWEST_BIN[item.internal_name] 48 | value["price_source"] = "BIN" 49 | else: 50 | value["base_price"] = item_price 51 | value["price_source"] = npc 52 | elif item.internal_name in data.BAZAAR: 53 | value["base_price"] = data.BAZAAR[item.internal_name] 54 | value["price_source"] = "Bazaar" 55 | elif item.internal_name in data.LOWEST_BIN: 56 | value["base_price"] = data.LOWEST_BIN[item.internal_name] 57 | value["price_source"] = "BIN" 58 | else: 59 | converted_name = item.name.upper().replace("- ", "").replace(" ", "_").replace("✪", "").replace("'", "").rstrip("_") # The Jerry price list uses the item name, not the internal_id. 60 | value["base_price"] = data.PRICES.get(converted_name, None) 61 | value["price_source"] = "Jerry" 62 | 63 | if value["base_price"] is None: 64 | value["base_price"] = 0 65 | value["price_source"] = "None" 66 | #============================================================================= 67 | # Hoe calculations 68 | if item.type == "HOE" and item.hoe_material_list is not None: 69 | value["price_source"] = "Calculated" 70 | value["base_price"] = 1_000_000 + 512*data.BAZAAR[item.hoe_material_list[0]] 71 | if item.hoe_level >= 2: 72 | value["base_price"] += 256*data.BAZAAR[item.hoe_material_list[1]] 73 | if item.hoe_level >= 3: 74 | value["base_price"] += 256*data.BAZAAR[item.hoe_material_list[2]] 75 | # Accessories of Power 76 | if item.internal_name in ["POWER_TALISMAN", "POWER_RING", "POWER_ARTIFACT"]: 77 | value["price_source"] = "Calculated" 78 | value["base_price"] = 45*data.BAZAAR.get("FLAWED_RUBY_GEM", 0) # Flawed Rubies 79 | if item.internal_name == "POWER_RING": 80 | value["base_price"] += 7*data.BAZAAR.get("FINE_RUBY_GEM", 0) + data.LOWEST_BIN.get("GEMSTONE_MIXTURE", 0) # Fine rubies + gem mix 81 | if item.internal_name == "POWER_ARTIFACT": 82 | value["base_price"] += 33*data.LOWEST_BIN.get("GEMSTONE_MIXTURE", 0) # More gemstone mixture 83 | #============================================================================= 84 | # Hot potato books: 85 | if item.hot_potatoes > 0: 86 | value["hot_potatoes"] = {} 87 | if item.hot_potatoes <= 10: 88 | value["hot_potatoes"]["hot_potato_books"] = item.hot_potatoes*data.BAZAAR["HOT_POTATO_BOOK"] 89 | else: 90 | value["hot_potatoes"]["hot_potato_books"] = 10*data.BAZAAR["HOT_POTATO_BOOK"] 91 | value["hot_potatoes"]["fuming_potato_books"] = (item.hot_potatoes-10)*data.BAZAAR["FUMING_POTATO_BOOK"] 92 | # Recombobulation 93 | if item.recombobulated and item.item_group is not None and value["price_source"] not in ["Bazaar", "None"]: 94 | value["recombobulator_value"] = data.BAZAAR["RECOMBOBULATOR_3000"] 95 | # Enchantments 96 | if item.enchantments: 97 | price = calculate_enchantments(data, price) 98 | # Reforge: 99 | if item.item_group is not None and item.reforge is not None: 100 | price = calculate_reforge_price(data, price) 101 | # Talisman enrichments 102 | if item.talisman_enrichment: 103 | value["talisman_enrichment"] = {item.talisman_enrichment: data.LOWEST_BIN.get("TALISMAN_ENRICHMENT_"+item.talisman_enrichment, 0)} 104 | # Dungeon items/stars 105 | if item.star_upgrades: 106 | price = calculate_dungeon_item(data, price) 107 | # Art of war 108 | if item.art_of_war: 109 | value["art_of_war_value"] = data.BAZAAR["THE_ART_OF_WAR"] 110 | # Wood singularity 111 | if item.wood_singularity: 112 | value["wood_singularty_value"] = data.BAZAAR["WOOD_SINGULARITY"] 113 | # Armor skins 114 | if item.skin: 115 | value["skin"] = {} 116 | value["skin"][item.skin] = data.LOWEST_BIN.get(item.skin, 0) 117 | # Power ability scrolls: (Gemstone ability scrolls) 118 | if item.power_ability_scroll: 119 | value["power_ability_scroll"] = {} 120 | value["power_ability_scroll"][item.power_ability_scroll] = data.LOWEST_BIN.get(item.power_ability_scroll, 0) 121 | # Gems 122 | if item.gems: 123 | value["gems"] = {} 124 | for gem, condition in item.gems.items(): 125 | gem_name = gem.removesuffix('_0').removesuffix('_1').removesuffix('_2').removesuffix('_3').removesuffix('_4').removesuffix('_5').removesuffix('_6') 126 | value["gems"][gem] = data.BAZAAR.get(f"{condition}_{gem_name}_GEM", 0) 127 | # Gemstone chambers 128 | if item.gemstone_chambers: 129 | value["gemstone_chambers"] = item.gemstone_chambers*data.LOWEST_BIN.get("GEMSTONE_CHAMBER", 0) 130 | # Farming for dummies books on hoes 131 | if item.farming_for_dummies: 132 | value["farming_for_dummies"] = item.farming_for_dummies*data.BAZAAR.get("FARMING_FOR_DUMMIES", 0) 133 | # Drills (upgrades) 134 | if item.type == "DRILL" and item.has_drill_upgrade: 135 | value["drill_upgrades"] = {} 136 | if item.drill_module_upgrade: 137 | value["drill_upgrades"][item.drill_module_upgrade] = data.LOWEST_BIN.get(item.drill_module_upgrade, 0) 138 | if item.drill_engine_upgrade: 139 | value["drill_upgrades"][item.drill_engine_upgrade] = data.LOWEST_BIN.get(item.drill_engine_upgrade, 0) 140 | if item.drill_tank_upgrade: 141 | value["drill_upgrades"][item.drill_tank_upgrade] = data.LOWEST_BIN.get(item.drill_tank_upgrade, 0) 142 | # Tuned transmission: 143 | if item.tuned_transmission: 144 | value["tuned_transmission"] = item.tuned_transmission*data.BAZAAR.get("TRANSMISSION_TUNER", 0) 145 | # Ethermerge 146 | if item.ethermerge: 147 | value["ethermerge"] = data.LOWEST_BIN.get("ETHERWARP_MERGER", 0)+data.LOWEST_BIN.get("ETHERWARP_CONDUIT", 0) 148 | # Winning bid for Midas Staff/Sword 149 | if item.winning_bid > 0 and item.internal_name in ["MIDAS_STAFF", "MIDAS_SWORD"]: 150 | value["winning_bid"] = item.winning_bid 151 | # Hyperion + Other scrolls (Necron's Blade Scrolls) 152 | if item.ability_scrolls: 153 | value["ability_scrolls_value"] = sum([data.BAZAAR.get(scroll, 0) for scroll in item.ability_scrolls]) 154 | # Dyes: 155 | if item.dye: 156 | value["dye"] = {} 157 | value["dye"][item.dye] = data.LOWEST_BIN.get(item.dye, 0) 158 | #================= 159 | 160 | price.value = value 161 | return price 162 | -------------------------------------------------------------------------------- /api/data/calculators/dungeon_calculator.py: -------------------------------------------------------------------------------- 1 | from data.constants.essence import ESSENCE_DICT 2 | 3 | MASTER_STAR_NAMES = ['first_master_star', 'second_master_star', 'third_master_star', 'fourth_master_star', 'fifth_master_star'] 4 | 5 | #ESSENCE_ICE 6 | 7 | def calculate_base_stars(data, price): 8 | item = price.item 9 | #print("Calc stars:", item.name, item.star_upgrades) 10 | essence_object = ESSENCE_DICT.get(item.internal_name.removeprefix("STARRED_"), None) 11 | if essence_object is None: 12 | #print("> CALC STARS FAILED:", item.internal_name, item.star_upgrades) 13 | return price 14 | essence_required = sum([essence_object[f"{i}"] for i in range(1, min(5, item.star_upgrades))]) 15 | essence_type = essence_object.get("type", "Spider") # Default to Spider, it's mid tier 16 | essence_type_value = data.BAZAAR.get("ESSENCE_"+essence_type.upper(), 0) 17 | essence_value = essence_type_value*essence_required 18 | 19 | # These convert to strings so they don't get counted 20 | price.value["stars"]["regular_stars"] = {"essence_required": f"{essence_required}", 21 | "essence_type": f"{essence_type} ({essence_type_value} each)", 22 | "total_essence_value": essence_value} # Needs to be an int 23 | return price 24 | 25 | 26 | def calculate_dungeon_item(data, price, print_prices=False): 27 | item = price.item 28 | 29 | price.value["stars"] = {} 30 | price = calculate_base_stars(data, price) 31 | 32 | if item.star_upgrades > 5: 33 | price.value["stars"]["master_stars"] = {} 34 | for i in range(0, item.star_upgrades-5): 35 | master_star_name = MASTER_STAR_NAMES[i] 36 | price.value["stars"]["master_stars"][master_star_name] = data.BAZAAR.get(master_star_name.upper(), 0) 37 | 38 | return price 39 | 40 | -------------------------------------------------------------------------------- /api/data/calculators/enchantment_calculator.py: -------------------------------------------------------------------------------- 1 | from data.constants.enchants_top import ENCHANTS_TOP 2 | from data.constants.enchantment_levels import ENCHANTMENT_LEVELS 3 | 4 | ROMAN_NUMERALS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI"] 5 | 6 | def calculate_enchanted_book(data, price): # For enchanted books 7 | 8 | element = price.item 9 | 10 | if "Anvil" in element.description_clean[0]: 11 | price.value["price_source"] = "None" 12 | price.value["base_price"] = 0 13 | return price 14 | 15 | rarity = element.description_clean[-1] 16 | first_line_of_desc = element.description_clean[0].split(" ") 17 | enchantment_type = " ".join(first_line_of_desc[:-1]).replace(" ", "_").upper() 18 | numeral_enchantment_level = first_line_of_desc[-1] 19 | 20 | enchantment_level = ROMAN_NUMERALS.index(numeral_enchantment_level)+1 21 | 22 | if f"ENCHANTMENT_{enchantment_type}_{enchantment_level}" in data.BAZAAR: 23 | price.value["price_source"] = "Bazaar" 24 | price.value["enchantments_value"] = data.BAZAAR[f"ENCHANTMENT_{enchantment_type}_{enchantment_level}"] 25 | 26 | elif f"{enchantment_type};{enchantment_level}" in data.LOWEST_BIN: 27 | price.value["price_source"] = "BIN" 28 | price.value["enchantments_value"] = data.LOWEST_BIN[f"{enchantment_type};{enchantment_level}"] 29 | 30 | else: 31 | price.value["price_source"] = "Jerry" 32 | price.value["enchantments_value"] = data.PRICES.get(f"{enchantment_type.lower()}_{enchantment_level}", 0) 33 | 34 | return price 35 | 36 | def calculate_enchantments(data, price): # For enchantments on items 37 | 38 | price.value["enchantments"] = {} 39 | 40 | #print("Calculating item enchantments") 41 | for enchantment, level in price.item.enchantments.items(): 42 | enchant_name = enchantment.upper() 43 | 44 | if f"ENCHANTMENT_{enchant_name}_{level}" in data.BAZAAR: 45 | enchant_price = data.BAZAAR[f"ENCHANTMENT_{enchant_name}_{level}"] 46 | 47 | elif enchant_name in ["CULTIVATING", "COMPACT", "CHAMPION", "HETACOMBS", "EXPERTISE"]: 48 | # Special case for enchants obtained through doing tasks such as breaking crops 49 | enchant_price = data.LOWEST_BIN.get(f"{enchant_name};{1}", 0) 50 | 51 | elif f"{enchant_name};{level}" in data.LOWEST_BIN: 52 | enchant_price = data.LOWEST_BIN[f"{enchant_name};{level}"] 53 | 54 | else: 55 | #print("Couldn't find on LOWEST_BIN as level 1, or on Bazaar") 56 | #print(f"ENCHANTMENT_{enchant_name}_{level}") 57 | enchant_price = 0 58 | 59 | price.value["enchantments"][f"{enchantment}_{level}"] = enchant_price 60 | 61 | return price 62 | -------------------------------------------------------------------------------- /api/data/calculators/main_calculator_handler.py: -------------------------------------------------------------------------------- 1 | from data.calculators.enchantment_calculator import calculate_enchanted_book 2 | from data.calculators.pet_calculator import calculate_pet 3 | from data.calculators.base_item_calculator import calculate_item 4 | from data.price_object import Price 5 | 6 | def calculate_container(data, elements, print_prices=False): 7 | prices = [] 8 | for element in elements: 9 | price = Price(element) 10 | 11 | if isinstance(element, dict) and ('candyUsed' in element.keys() or 'active' in element.keys()): 12 | price_object = calculate_pet(data, price, print_prices) 13 | 14 | elif element.internal_name == "ENCHANTED_BOOK": 15 | price_object = calculate_enchanted_book(data, price) 16 | 17 | elif element.internal_name == "PET" and element.name != "Unknown Pet": 18 | pet_info = price.item.pet_info 19 | element = {'uuid': None, 'type': pet_info['type'], 'exp': pet_info['exp'], 'active': False, 'tier': pet_info["tier"], 20 | 'candyUsed': pet_info.get('candyUsed', 0)} 21 | if "skin" in pet_info: 22 | element['skin'] = pet_info['skin'] 23 | if "heldItem" in pet_info: 24 | element['heldItem'] = pet_info['heldItem'] 25 | price_object = calculate_pet(data, Price(element), print_prices) 26 | else: 27 | price_object = calculate_item(data, price, print_prices) 28 | 29 | if price_object is not None: 30 | price_object.calculate_total() 31 | prices.append(price_object) 32 | 33 | return prices 34 | -------------------------------------------------------------------------------- /api/data/calculators/pet_calculator.py: -------------------------------------------------------------------------------- 1 | from data.constants.pets import PET_LEVELS 2 | PET_LEVELS = PET_LEVELS+[50000000000000000000] 3 | 4 | RARITY_OFFSET = {"COMMON": 0, "UNCOMMON": 6, "RARE": 11, "EPIC": 16, "LEGENDARY": 20, "MYTHIC": 20} 5 | TIERS = list(RARITY_OFFSET.keys()) 6 | 7 | COINS_PER_XP = 0.2 8 | COINS_PER_XP_TWO_HUNDRED = 3 9 | PET_LEVELS_SUMMED = sum(PET_LEVELS[RARITY_OFFSET["LEGENDARY"]:-1]) 10 | 11 | def get_pet_level(pet): 12 | pet_xp = pet["exp"] 13 | xp_offset = RARITY_OFFSET[pet["tier"]] 14 | 15 | pet_level = 1 16 | while pet_xp > 0 and pet_level < 100: 17 | pet_xp -= PET_LEVELS[pet_level+xp_offset] 18 | pet_level += 1 19 | 20 | return pet_level 21 | 22 | def calculate_pet(data, price, print_prices): 23 | 24 | pet = price.item 25 | value = price.value 26 | 27 | if pet['type'] == "GOLDEN_DRAGON" and pet['exp'] > PET_LEVELS_SUMMED: 28 | # Dragon required 1.8m xp for each level 103-200, 29 | # 101 is insta recieved on hatching and 102 is weirdly broken... 30 | pet_level = min(100+int((pet["exp"]-PET_LEVELS_SUMMED)//1_886_700)+2, 200) 31 | else: 32 | pet_level = get_pet_level(pet) 33 | 34 | value["pet_level"] = f"{pet_level}" 35 | 36 | ####################################################################################### 37 | # BASE VALUE 38 | if f"{pet['type']};{TIERS.index(pet['tier'])}" in data.LOWEST_BIN: 39 | # Try from LOWEST_BIN 40 | value["base_price"] = data.LOWEST_BIN[f"{pet['type']};{TIERS.index(pet['tier'])}"] 41 | value["price_source"] = "BIN" 42 | else: 43 | # Try from Jerry's list 44 | value["base_price"] = data.PRICES.get(f"LVL_1_{pet['tier']}_{pet['type']}", 0) # LVL_1_COMMON_ENDERMAN 45 | value["price_source"] = "Jerry" 46 | 47 | ####################################################################################### 48 | # These can't be sold, so ignore their pet xp value and items 49 | if pet['type'] == "GRANDMA_WOLF": 50 | value["pet_level_bonus"] = {} 51 | value["pet_level_bonus"]["worth"] = 0 52 | price.value = value 53 | return price 54 | 55 | ####################################################################################### 56 | # PET ITEM VALUE 57 | pet_held_item = pet.get("heldItem", "") 58 | if pet_held_item: 59 | value["held_item"] = {} 60 | value["held_item"]["item"] = pet_held_item 61 | value["held_item"]["value"] = data.LOWEST_BIN.get(pet_held_item, 0) 62 | value["held_item"]["price_source"] = "BIN" 63 | 64 | ####################################################################################### 65 | # PET SKIN VALUE 66 | pet_skin = pet.get("skin", False) 67 | if pet_skin: 68 | value["pet_skin"] = {} 69 | value["pet_skin"]["item"] = "PET_SKIN_"+pet['skin'] 70 | value["pet_skin"]["value"] = data.LOWEST_BIN.get("PET_SKIN_"+pet['skin'], 0) 71 | value["pet_skin"]["price_source"] = "BIN" 72 | 73 | ####################################################################################### 74 | # PET LEVEL BONUS 75 | value["pet_level_bonus"] = {} 76 | value["pet_level_bonus"]["amount"] = f"{int(pet['exp'])} xp" 77 | value["pet_level_bonus"]["price_source"] = "Calculated" 78 | 79 | if pet_level >= 100: 80 | offset = RARITY_OFFSET[pet["tier"]]-1 81 | level_100_amount = sum(PET_LEVELS[offset:100+offset]) 82 | pet_xp_capped = min(pet["exp"], level_100_amount) 83 | else: 84 | pet_xp_capped = pet["exp"] 85 | 86 | value["pet_level_bonus"]["worth"] = int(pet_xp_capped*COINS_PER_XP) # 5 Xp = 1 coin, seems about right but this is subjective. 87 | 88 | if pet_level > 100: # If it's level is over 100, i.e. it's a Golden Dragon, give each level's worth of xp a 3x multiplier 89 | value["pet_level_bonus"]["worth"] += (pet_level-100)*1_886_700*COINS_PER_XP_TWO_HUNDRED 90 | 91 | ####################################################################################### 92 | 93 | price.value = value 94 | return price 95 | -------------------------------------------------------------------------------- /api/data/constants/collector.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | def fetch_prices(): 5 | print("Pre-fetch prices") 6 | #================================================================== 7 | # BUY IT NOW 8 | file, var_name, link = ("lowest_bin", "LOWEST_BIN", "http://moulberry.codes/lowestbin.json") 9 | try: 10 | request = requests.get(link).json() 11 | 12 | LOWEST_BIN = dict([(k, int(v)) for k, v in request.items()]) 13 | except Exception as e: 14 | print(e) 15 | print("ERROR: Failed to load LOWEST_BIN, falling back to previous state instead.") 16 | from data.constants.lowest_bin import LOWEST_BIN 17 | 18 | with open(f"data/constants/{file}.py", 'w') as file: 19 | file.write(f"{var_name} = "+json.dumps(LOWEST_BIN, indent=4)) 20 | 21 | print(f"Loaded in {var_name}") 22 | #================================================================== 23 | # BAZAAR 24 | file, var_name, link = ("bazaar", "BAZAAR", "https://api.hypixel.net/skyblock/bazaar") 25 | BAZAAR = {} 26 | 27 | try: 28 | result = requests.get(link).json() 29 | for product in result["products"]: 30 | BAZAAR[product] = int(result["products"][product]['quick_status']['buyPrice']) 31 | except: 32 | print("ERROR: Failed to load BAZAAR, falling back to previous state instead") 33 | from data.constants.bazaar import BAZAAR 34 | 35 | with open(f"data/constants/{file}.py", 'w') as file: 36 | file.write(f"{var_name} = "+json.dumps(BAZAAR, indent=4)) 37 | 38 | print(f"Loaded in {var_name}") 39 | #================================================================== 40 | # JERRY'S PRICE LIST 41 | file, var_name, link = ("jerry_price_list", "PRICES", "https://raw.githubusercontent.com/skyblockz/pricecheckbot/master/data.json") 42 | PRICES = {} 43 | 44 | try: 45 | result = requests.get(link).json() 46 | for item in result: 47 | name = item["name"].upper() 48 | price = int((item["low"]+item["hi"])/2) 49 | PRICES[name] = price 50 | except: 51 | print("ERROR: Failed to load PRICES, falling back to previous state instead") 52 | from data.constants.jerry_price_list import PRICES 53 | 54 | with open(f"data/constants/{file}.py", 'w') as file: 55 | file.write(f"{var_name} = "+json.dumps(PRICES, indent=4)) 56 | 57 | print(f"Loaded in {var_name}") 58 | #================================================================== 59 | # MANUAL THINGS NPC'S SELL 60 | from data.constants.npc_items import NPC_ITEMS 61 | print("Loaded in NPC_ITEMS") 62 | 63 | return BAZAAR, LOWEST_BIN, PRICES, NPC_ITEMS 64 | 65 | def fetch_constants(): 66 | #================================================================== 67 | # CONSTANTS 68 | ENCHANTS = ("enchants", "ENCHANTS_DICT", "https://raw.githubusercontent.com/Moulberry/NotEnoughUpdates-REPO/master/constants/enchants.json") 69 | ESSENCE = ("essence", "ESSENCE_DICT", "https://raw.githubusercontent.com/Moulberry/NotEnoughUpdates-REPO/master/constants/essencecosts.json") 70 | 71 | for file, var_name, link in (ENCHANTS, ESSENCE): 72 | result = requests.get(link).json() 73 | 74 | with open(f"{file}.py", 'w') as file: 75 | file.write(f"{var_name} = "+json.dumps(result, indent=4)) 76 | print(f"Loaded in {var_name}") 77 | 78 | #================================================================== 79 | # REFORGE STONES 80 | file, var_name, link = ("pets", "PET_LEVELS", "https://raw.githubusercontent.com/Moulberry/NotEnoughUpdates-REPO/master/constants/pets.json") 81 | result = requests.get(link).json() 82 | 83 | with open(f"{file}.py", 'w') as file: 84 | file.write(f"{var_name} = "+json.dumps(result['pet_levels'], indent=4)) 85 | print(f"Loaded in {var_name}") 86 | 87 | #================================================================== 88 | # REFORGE STONES 89 | file, var_name, link = ("reforges", "REFORGE_DICT", "https://raw.githubusercontent.com/Moulberry/NotEnoughUpdates-REPO/master/constants/reforgestones.json") 90 | result = requests.get(link).json() 91 | REFORGE_DICT = {} 92 | 93 | with open(f"{file}.py", 'w') as file: 94 | for internal_name in result: 95 | reforge_name = result[internal_name]["reforgeName"].lower() 96 | item_type = result[internal_name]["itemTypes"] 97 | if "/" in item_type: 98 | item_list = item_type.split("/") 99 | elif item_type == "ARMOR": 100 | item_list = ("HELMET", "CHESTPLATE", "LEGGINGS", "BOOTS") 101 | else: 102 | item_list = (item_type, ) 103 | 104 | for item in item_list: 105 | REFORGE_DICT[reforge_name+";"+item] = {"INTERNAL_NAME": internal_name, 106 | "REFORGE_COST": result[internal_name]["reforgeCosts"],} 107 | file.write(f"{var_name} = "+json.dumps(REFORGE_DICT, indent=4)) 108 | print(f"Loaded in {var_name}") 109 | #================================================================== 110 | # ENCHANTS TOP NORMALLY ACHIEVABLE LEVEL 111 | # Moulberry removed the encahnts_min_level section, but as we already have it, we can just leave it and update it manually ): 112 | file, var_name, link = ("enchants_top", "ENCHANTS_TOP", "https://raw.githubusercontent.com/Moulberry/NotEnoughUpdates-REPO/master/constants/enchants.json") 113 | result = requests.get(link).json() 114 | ''' 115 | ENCHANTS_TOP = result["enchants_min_level"] 116 | 117 | with open(f"{file}.py", 'w') as file: 118 | file.write(f"{var_name} = "+json.dumps(ENCHANTS_TOP, indent=4)) 119 | 120 | print(f"Loaded in {var_name}") 121 | ''' 122 | #========================= 123 | # Enchants that can be got at the enchanting table (not bin anymore) 124 | file, var_name = ("enchantment_levels", "ENCHANTMENT_LEVELS") 125 | ENCHANTS_LEVELS = result["enchants_xp_cost"] 126 | 127 | with open(f"{file}.py", 'w') as file: 128 | file.write(f"{var_name} = "+json.dumps(ENCHANTS_LEVELS, indent=4)) 129 | 130 | print(f"Loaded in {var_name}") 131 | 132 | if __name__ == "__main__": 133 | fetch_constants() 134 | -------------------------------------------------------------------------------- /api/data/constants/enchantment_levels.py: -------------------------------------------------------------------------------- 1 | ENCHANTMENT_LEVELS = { 2 | "sharpness": [ 3 | 10, 4 | 15, 5 | 20, 6 | 25, 7 | 30 8 | ], 9 | "smite": [ 10 | 10, 11 | 15, 12 | 20, 13 | 25, 14 | 30 15 | ], 16 | "bane_of_arthropods": [ 17 | 10, 18 | 15, 19 | 20, 20 | 25, 21 | 30 22 | ], 23 | "telekinesis": [ 24 | 5 25 | ], 26 | "knockback": [ 27 | 15, 28 | 30 29 | ], 30 | "fire_aspect": [ 31 | 15, 32 | 30 33 | ], 34 | "experience": [ 35 | 15, 36 | 30, 37 | 45 38 | ], 39 | "looting": [ 40 | 15, 41 | 30, 42 | 45 43 | ], 44 | "scavenger": [ 45 | 10, 46 | 20, 47 | 30 48 | ], 49 | "luck": [ 50 | 10, 51 | 20, 52 | 30, 53 | 40, 54 | 50 55 | ], 56 | "cubism": [ 57 | 10, 58 | 20, 59 | 30, 60 | 40, 61 | 50 62 | ], 63 | "cleave": [ 64 | 10, 65 | 20, 66 | 30, 67 | 40, 68 | 50 69 | ], 70 | "life_steal": [ 71 | 20, 72 | 25, 73 | 30 74 | ], 75 | "giant_killer": [ 76 | 10, 77 | 20, 78 | 30, 79 | 40, 80 | 50 81 | ], 82 | "critical": [ 83 | 10, 84 | 20, 85 | 30, 86 | 40, 87 | 50 88 | ], 89 | "first_strike": [ 90 | 20, 91 | 30, 92 | 40, 93 | 75 94 | ], 95 | "ender_slayer": [ 96 | 10, 97 | 20, 98 | 25, 99 | 30, 100 | 40 101 | ], 102 | "impaling": [ 103 | 30, 104 | 40, 105 | 50 106 | ], 107 | "execute": [ 108 | 20, 109 | 25, 110 | 30, 111 | 40, 112 | 50 113 | ], 114 | "thunderlord": [ 115 | 20, 116 | 25, 117 | 30, 118 | 40, 119 | 50 120 | ], 121 | "lethality": [ 122 | 20, 123 | 25, 124 | 30, 125 | 40, 126 | 50 127 | ], 128 | "syphon": [ 129 | 20, 130 | 25, 131 | 30 132 | ], 133 | "vampirism": [ 134 | 20, 135 | 25, 136 | 30, 137 | 40, 138 | 50 139 | ], 140 | "venomous": [ 141 | 20, 142 | 25, 143 | 30, 144 | 40, 145 | 50 146 | ], 147 | "triple_strike": [ 148 | 20, 149 | 30, 150 | 40, 151 | 75 152 | ], 153 | "thunderbolt": [ 154 | 20, 155 | 25, 156 | 30, 157 | 40, 158 | 50 159 | ], 160 | "PROSECUTE": [ 161 | 20, 162 | 25, 163 | 30, 164 | 40, 165 | 50 166 | ], 167 | "titan_killer": [ 168 | 10, 169 | 20, 170 | 30, 171 | 40, 172 | 50 173 | ], 174 | "power": [ 175 | 10, 176 | 20, 177 | 30, 178 | 40, 179 | 50 180 | ], 181 | "punch": [ 182 | 15, 183 | 30 184 | ], 185 | "flame": [ 186 | 25 187 | ], 188 | "infinite_quiver": [ 189 | 10, 190 | 15, 191 | 20, 192 | 25, 193 | 30 194 | ], 195 | "snipe": [ 196 | 20, 197 | 25, 198 | 30 199 | ], 200 | "aiming": [ 201 | 10, 202 | 20, 203 | 30, 204 | 40, 205 | 50 206 | ], 207 | "chance": [ 208 | 15, 209 | 30, 210 | 45 211 | ], 212 | "piercing": [ 213 | 30 214 | ], 215 | "efficiency": [ 216 | 10, 217 | 15, 218 | 20, 219 | 25, 220 | 30 221 | ], 222 | "smelting_touch": [ 223 | 5 224 | ], 225 | "silk_touch": [ 226 | 10 227 | ], 228 | "fortune": [ 229 | 15, 230 | 30, 231 | 45 232 | ], 233 | "lure": [ 234 | 10, 235 | 15, 236 | 20, 237 | 25, 238 | 30 239 | ], 240 | "luck_of_the_sea": [ 241 | 10, 242 | 15, 243 | 20, 244 | 25, 245 | 30 246 | ], 247 | "angler": [ 248 | 10, 249 | 20, 250 | 30, 251 | 40, 252 | 50 253 | ], 254 | "blessing": [ 255 | 10, 256 | 20, 257 | 30, 258 | 40, 259 | 50 260 | ], 261 | "magnet": [ 262 | 20, 263 | 25, 264 | 30, 265 | 40, 266 | 50 267 | ], 268 | "frail": [ 269 | 20, 270 | 25, 271 | 30, 272 | 40, 273 | 50 274 | ], 275 | "pristine": [ 276 | 25, 277 | 25, 278 | 25, 279 | 25 280 | ], 281 | "caster": [ 282 | 20, 283 | 25, 284 | 30, 285 | 40, 286 | 50 287 | ], 288 | "spiked_hook": [ 289 | 10, 290 | 20, 291 | 30, 292 | 40, 293 | 50 294 | ], 295 | "harvesting": [ 296 | 5, 297 | 10, 298 | 15, 299 | 20, 300 | 25 301 | ], 302 | "projectile_protection": [ 303 | 10, 304 | 15, 305 | 20, 306 | 25, 307 | 30 308 | ], 309 | "protection": [ 310 | 10, 311 | 15, 312 | 20, 313 | 25, 314 | 30 315 | ], 316 | "blast_protection": [ 317 | 10, 318 | 15, 319 | 20, 320 | 25, 321 | 30 322 | ], 323 | "fire_protection": [ 324 | 10, 325 | 15, 326 | 20, 327 | 25, 328 | 30 329 | ], 330 | "respiration": [ 331 | 10, 332 | 20, 333 | 30 334 | ], 335 | "aqua_affinity": [ 336 | 15 337 | ], 338 | "thorns": [ 339 | 15, 340 | 30, 341 | 45 342 | ], 343 | "growth": [ 344 | 10, 345 | 20, 346 | 30, 347 | 40, 348 | 50 349 | ], 350 | "feather_falling": [ 351 | 10, 352 | 15, 353 | 20, 354 | 25, 355 | 30 356 | ], 357 | "frost_walker": [ 358 | 10, 359 | 20 360 | ], 361 | "depth_strider": [ 362 | 10, 363 | 20, 364 | 30 365 | ], 366 | "rainbow": [ 367 | 10 368 | ] 369 | } -------------------------------------------------------------------------------- /api/data/constants/enchants_top.py: -------------------------------------------------------------------------------- 1 | ENCHANTS_TOP = { 2 | "bane_of_arthropods": 5, 3 | "big_brain": 5, 4 | "chance": 3, 5 | "cleave": 5, 6 | "critical": 5, 7 | "ender_slayer": 5, 8 | "execute": 5, 9 | "PROSECUTE": 5, 10 | "fire_aspect": 2, 11 | "first_strike": 4, 12 | "triple-strike": 5, 13 | "giant_killer": 5, 14 | "titan_killer": 5, 15 | "knockback": 2, 16 | "lethality": 5, 17 | "life_steal": 3, 18 | "syphon": 3, 19 | "luck": 5, 20 | "scavenger": 3, 21 | "sharpness": 5, 22 | "smite": 5, 23 | "thunderlord": 5, 24 | "thunderbolt": 5, 25 | "vampirism": 5, 26 | "venomous": 5, 27 | "vicious": 4, 28 | "ultimate_chimera": 5, 29 | "ultimate_combo": 5, 30 | "ultimate_swarm": 5, 31 | "ultimate_wise": 5, 32 | "aiming": 5, 33 | "cubism": 5, 34 | "dragon_hunter": 5, 35 | "flame": 1, 36 | "impaling": 3, 37 | "infinite_quiver": 5, 38 | "piercing": 1, 39 | "overload": 5, 40 | "power": 5, 41 | "punch": 2, 42 | "snipe": 3, 43 | "ultimate_rend": 5, 44 | "telekinesis": 1, 45 | "silk_touch": 1, 46 | "compact": 10, 47 | "experience": 4, 48 | "fortune": 3, 49 | "smelting_touch": 1, 50 | "angler": 5, 51 | "blessing": 5, 52 | "caster": 5, 53 | "expertise": 10, 54 | "frail": 5, 55 | "looting": 3, 56 | "luck_of_the_sea": 5, 57 | "lure": 5, 58 | "magnet": 5, 59 | "spiked_hook": 5, 60 | "efficiency": 5, 61 | "replenish": 1, 62 | "harvesting": 5, 63 | "aqua_affinity": 1, 64 | "counter_strike": 5, 65 | "true_protection": 1, 66 | "protection": 5, 67 | "fire_protection": 5, 68 | "blast_protection": 5, 69 | "projectile_protection": 5, 70 | "growth": 5, 71 | "sugar_rush": 3, 72 | "rejuvenate": 5, 73 | "respite": 5, 74 | "feather_falling": 5, 75 | "depth_strider": 3, 76 | "thorns": 3, 77 | "turbo_wheat": 5, 78 | "turbo_cane": 5, 79 | "turbo_warts": 5, 80 | "turbo_carrot": 5, 81 | "turbo_potato": 5, 82 | "turbo_mushrooms": 5, 83 | "turbo_cactus": 5, 84 | "turbo_coco": 5, 85 | "turbo_melon": 5, 86 | "turbo_pumpkin": 5, 87 | "ultimate_one_for_all": 1, 88 | "ultimate_soul_eater": 5, 89 | "ultimate_bank": 5, 90 | "ultimate_last_stand": 5, 91 | "ultimate_legion": 5, 92 | "ultimate_no_pain_no_gain": 5, 93 | "ultimate_wisdom": 5, 94 | "cultivating": 10 95 | } -------------------------------------------------------------------------------- /api/data/constants/pets.py: -------------------------------------------------------------------------------- 1 | PET_LEVELS = [ 2 | 100, 3 | 110, 4 | 120, 5 | 130, 6 | 145, 7 | 160, 8 | 175, 9 | 190, 10 | 210, 11 | 230, 12 | 250, 13 | 275, 14 | 300, 15 | 330, 16 | 360, 17 | 400, 18 | 440, 19 | 490, 20 | 540, 21 | 600, 22 | 660, 23 | 730, 24 | 800, 25 | 880, 26 | 960, 27 | 1050, 28 | 1150, 29 | 1260, 30 | 1380, 31 | 1510, 32 | 1650, 33 | 1800, 34 | 1960, 35 | 2130, 36 | 2310, 37 | 2500, 38 | 2700, 39 | 2920, 40 | 3160, 41 | 3420, 42 | 3700, 43 | 4000, 44 | 4350, 45 | 4750, 46 | 5200, 47 | 5700, 48 | 6300, 49 | 7000, 50 | 7800, 51 | 8700, 52 | 9700, 53 | 10800, 54 | 12000, 55 | 13300, 56 | 14700, 57 | 16200, 58 | 17800, 59 | 19500, 60 | 21300, 61 | 23200, 62 | 25200, 63 | 27400, 64 | 29800, 65 | 32400, 66 | 35200, 67 | 38200, 68 | 41400, 69 | 44800, 70 | 48400, 71 | 52200, 72 | 56200, 73 | 60400, 74 | 64800, 75 | 69400, 76 | 74200, 77 | 79200, 78 | 84700, 79 | 90700, 80 | 97200, 81 | 104200, 82 | 111700, 83 | 119700, 84 | 128200, 85 | 137200, 86 | 146700, 87 | 156700, 88 | 167700, 89 | 179700, 90 | 192700, 91 | 206700, 92 | 221700, 93 | 237700, 94 | 254700, 95 | 272700, 96 | 291700, 97 | 311700, 98 | 333700, 99 | 357700, 100 | 383700, 101 | 411700, 102 | 441700, 103 | 476700, 104 | 516700, 105 | 561700, 106 | 611700, 107 | 666700, 108 | 726700, 109 | 791700, 110 | 861700, 111 | 936700, 112 | 1016700, 113 | 1101700, 114 | 1191700, 115 | 1286700, 116 | 1386700, 117 | 1496700, 118 | 1616700, 119 | 1746700, 120 | 1886700 121 | ] -------------------------------------------------------------------------------- /api/data/container_handler.py: -------------------------------------------------------------------------------- 1 | from data.decode_container import parse_container 2 | from data.calculators.main_calculator_handler import calculate_container 3 | from exceptions import InvalidProfileException, NoProfilesException, InvalidUUIDException 4 | 5 | def get_storage(player_data): 6 | if not player_data.get("backpack_contents", False): 7 | return [] 8 | storage_items = [] 9 | for i in range(0, 19): 10 | page = player_data["backpack_contents"].get(str(i), {"data": []}) 11 | storage_items.extend(parse_container(page["data"])) 12 | 13 | return storage_items 14 | 15 | def get_data(profile_data, uuid, profile_name): 16 | uuid = uuid.replace("-", "") 17 | if profile_name not in ['None', 'latest', None]: 18 | if not (profiles := [x for x in profile_data if x["cute_name"].lower() == profile_name.lower()]): 19 | raise InvalidProfileException 20 | profile = profiles[0] 21 | else: 22 | if not any([uuid in x['members'] for x in profile_data]): 23 | raise InvalidUUIDException 24 | 25 | valid_profiles = [x for x in profile_data if uuid in x['members'] and "selected" in x] 26 | if not valid_profiles: 27 | raise NoProfilesException 28 | profile = max(valid_profiles, key=lambda x: x['selected']) 29 | 30 | if uuid not in profile['members'].keys(): 31 | raise InvalidUUIDException 32 | 33 | other_data = profile 34 | player_data = profile["members"][uuid] 35 | 36 | return player_data, other_data 37 | 38 | 39 | def get_containers(data, profile_data, uuid, profile_name): 40 | # Parse/Grab data 41 | player_data, other_data = get_data(profile_data, uuid, profile_name) 42 | 43 | if player_data is None: 44 | return None, None 45 | 46 | debug_items = False 47 | # Get item groupings 48 | if debug_items: 49 | print("Pre ALL parse containers, now testing inv_contents") 50 | inv_contents = parse_container(player_data.get("inv_contents", {"data": []})['data']) 51 | if debug_items: 52 | print("inv_contents parsed properly, now testing talisman_bag") 53 | talisman_bag = parse_container(player_data.get("talisman_bag", {"data": []})['data']) 54 | if debug_items: 55 | print("talisman_bag parsed properly, now testing equipment") 56 | equipment = parse_container(player_data.get("equippment_contents", {"data": []})['data']) 57 | if debug_items: 58 | print("equipment parsed properly, now testing ender_chest") 59 | ender_chest = parse_container(player_data.get("ender_chest_contents", {"data": []})['data']) 60 | if debug_items: 61 | print("ender_chest parsed properly, now testing armour") 62 | armour = parse_container(player_data.get("inv_armor", {"data": []})['data']) 63 | if debug_items: 64 | print("armour parsed properly, now testing wardrobe") 65 | wardrobe = parse_container(player_data.get("wardrobe_contents", {"data": []})['data']) 66 | if debug_items: 67 | print("wardrobe parsed properly, now testing personal_vault") 68 | personal_vault = parse_container(player_data.get("personal_vault_contents", {"data": []})['data']) 69 | if debug_items: 70 | print("personal_vault parsed properly, now testing storage_items") 71 | storage_items = get_storage(player_data) 72 | if debug_items: 73 | print("storage_items parsed properly, now testing pet_items") 74 | pet_items = player_data.get("pets", []) 75 | if debug_items: 76 | print("pet items parsed properly, all parsing COMPLETE") 77 | 78 | if debug_items: 79 | print("Testing all `calculate_container`s") 80 | calculate_container(data, inv_contents) 81 | print("Post inv") 82 | calculate_container(data, talisman_bag) 83 | calculate_container(data, equipment) 84 | calculate_container(data, ender_chest) 85 | calculate_container(data, armour) 86 | calculate_container(data, wardrobe) 87 | calculate_container(data, personal_vault) 88 | calculate_container(data, storage_items) 89 | calculate_container(data, pet_items) 90 | print("All calculated properly") 91 | 92 | return ({ 93 | "profile_name": other_data["cute_name"], 94 | "profile_type": other_data.get("game_mode", "regular"), 95 | }, 96 | { 97 | "inventory": calculate_container(data, inv_contents), 98 | "accessories": calculate_container(data, talisman_bag), 99 | "equipment": calculate_container(data, equipment), 100 | "ender_chest": calculate_container(data, ender_chest), 101 | "armor": calculate_container(data, armour), 102 | "wardrobe": calculate_container(data, wardrobe), 103 | "vault": calculate_container(data, personal_vault), 104 | "storage": calculate_container(data, storage_items), 105 | "pets": calculate_container(data, pet_items) 106 | }, 107 | { 108 | "purse": int(player_data.get("coin_purse", 0)), # For some reason, purse contains a bunch of extra decimal places. 109 | "banking": int(other_data.get("banking", {"balance": 0}).get("balance", 0)) 110 | }, 111 | ) 112 | -------------------------------------------------------------------------------- /api/data/decode_container.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | from gzip import decompress 3 | from io import BytesIO 4 | from struct import unpack 5 | 6 | from data.item_object import Item 7 | 8 | def parse_container(raw): 9 | """Takes a raw string representing inventory data. Returns a json object with the inventory's contents""" 10 | if raw == []: return [] 11 | 12 | raw = BytesIO(decompress(b64decode(raw))) # Unzip raw string from the api 13 | 14 | def read(type, length): 15 | if type in 'chil': 16 | return int.from_bytes(raw.read(length), byteorder='big') 17 | if type == 's': 18 | return raw.read(length).decode('utf-8') 19 | return unpack('>' + type, raw.read(length))[0] 20 | 21 | def parse_list(): 22 | subtype = read('c', 1) 23 | payload = [] 24 | for _ in range(read('i', 4)): 25 | parse_next_tag(payload, subtype) 26 | return payload 27 | 28 | def parse_compound(): 29 | payload = {} 30 | while parse_next_tag(payload) != 0: # Parse tags until we find an endcap (type == 0) 31 | pass # Nothing needs to happen here 32 | return payload 33 | 34 | payloads = { 35 | 1: lambda: read('c', 1), # Byte 36 | 2: lambda: read('h', 2), # Short 37 | 3: lambda: read('i', 4), # Int 38 | 4: lambda: read('l', 8), # Long 39 | 5: lambda: read('f', 4), # Float 40 | 6: lambda: read('d', 8), # Double 41 | 7: lambda: raw.read(read('i', 4)), # Byte Array 42 | 8: lambda: read('s', read('h', 2)), # String 43 | 9: parse_list, # List 44 | 10: parse_compound, # Compound 45 | 11: lambda: [read('i', 4) for _ in range(read('i', 4))], # Int Array 46 | 12: lambda: [read('l', 8) for _ in range(read('i', 4))] # Long Array 47 | } 48 | 49 | def parse_next_tag(dictionary, tag_id=None): 50 | if tag_id is None: # Are we inside a list? 51 | tag_id = read('c', 1) 52 | if tag_id == 0: # Is this the end of a compound? 53 | return 0 54 | name = read('s', read('h', 2)) 55 | 56 | payload = payloads[tag_id]() 57 | if isinstance(dictionary, dict): 58 | dictionary[name] = payload 59 | else: 60 | dictionary.append(payload) 61 | 62 | raw.read(3) # Remove file header (we ingore footer) 63 | root = {} 64 | parse_next_tag(root) 65 | return [Item(x) for x in root['i'] if x] 66 | -------------------------------------------------------------------------------- /api/data/price_object.py: -------------------------------------------------------------------------------- 1 | def generate_tree(info_dict, current_list, current_indent): 2 | if isinstance(info_dict, dict): 3 | if len(list(info_dict.keys())) == 0: 4 | return 0 5 | for key, value in info_dict.items(): 6 | if isinstance(value, dict): 7 | current_list.append(f"{current_indent}{key}") 8 | generate_tree(value, current_list, current_indent+" ") 9 | else: 10 | current_list.append(f"{current_indent}{key}: {value}") 11 | return current_list 12 | 13 | def search_tree(info_dict): 14 | branch_value = 0 15 | if isinstance(info_dict, dict): 16 | if len(list(info_dict.keys())) == 0: 17 | return 0 18 | for key, value in info_dict.items(): 19 | if isinstance(value, dict): 20 | branch_value += search_tree(value) 21 | else: 22 | if not isinstance(value, str): 23 | branch_value += value 24 | return branch_value 25 | 26 | 27 | class Price(): 28 | def __init__(self, item): 29 | self.item = item 30 | self.value = {} # need this 31 | self.total = 0 32 | 33 | def display_output(self): 34 | print(self.item.internal_name) 35 | lines = generate_tree(self.value, [], " ") 36 | print("\n".join(lines)) 37 | total = search_tree(self.value) 38 | print(f"Total: {total}") 39 | 40 | def to_dump_string(self): 41 | ''' 42 | Used for the website dump (tree) to show it all. 43 | ''' 44 | if isinstance(self.item, dict): 45 | start = f"Level {self.value['pet_level']} {self.item['type'].replace('_', ' ').title()}\n" 46 | else: 47 | start = f"{self.item.internal_name}\n" 48 | 49 | return start+"\n".join(generate_tree(self.value, [], " "))+f"\nTotal Value: {search_tree(self.value)}"+"\n" 50 | 51 | 52 | def calculate_total(self): 53 | self.total = search_tree(self.value) 54 | if not isinstance(self.item, dict): 55 | self.total *= self.item.stack_size 56 | self.total = max(self.total, 0) # I shouldn't have to do this... Hypixel why... 57 | return self.total 58 | 59 | 60 | def to_dict(self): 61 | ''' 62 | returns a dictionary with all the needed information, included the total number, 63 | all the attributes that make up the value, as well as the attributes that make 64 | up the item object itself 65 | ''' 66 | item = self.item if isinstance(self.item, dict) else self.item.to_dict() 67 | 68 | return {"total": self.total, 69 | "value": self.value, 70 | "item": item, 71 | } 72 | -------------------------------------------------------------------------------- /api/endpoints/dump.py: -------------------------------------------------------------------------------- 1 | from data.container_handler import get_containers 2 | 3 | async def get_dump_dict(data, profile_data, uuid, profile_name): 4 | profile_data, containers, extras = get_containers(data, profile_data, uuid, profile_name) 5 | 6 | if profile_data is None: 7 | return None 8 | 9 | data = { 10 | "profile_data": profile_data, 11 | "purse": {"total": str(extras["purse"])}, 12 | "banking": {"total": str(extras["banking"])}, 13 | } 14 | 15 | for container in ("inventory", "accessories", "ender_chest", "armor", "wardrobe", "vault", "storage", "pets"): 16 | top_x = sorted(containers[container], key=lambda x: x.total, reverse=True) 17 | prices = [x.to_dict() for x in top_x] 18 | data[container] = { 19 | "total": f"{sum(x.total for x in containers[container])}", 20 | "prices": prices, 21 | } 22 | 23 | return data 24 | -------------------------------------------------------------------------------- /api/endpoints/groups.py: -------------------------------------------------------------------------------- 1 | from data.container_handler import get_containers 2 | 3 | async def get_groups_value(data, profile_data, uuid, profile_name): 4 | profile_data, containers, extras = get_containers(data, profile_data, uuid, profile_name) 5 | 6 | if profile_data is None: 7 | return None 8 | 9 | inventory_total = sum(x.total for x in containers["inventory"]) 10 | accessories_total = sum(x.total for x in containers["accessories"]) 11 | equipment_total = sum(x.total for x in containers["equippment_contents"]) 12 | ender_chest_total = sum(x.total for x in containers["ender_chest"]) 13 | armour_total = sum(x.total for x in containers["armor"]) 14 | wardrobe_total = sum(x.total for x in containers["wardrobe"]) 15 | vault_total = sum(x.total for x in containers["vault"]) 16 | storage_total = sum(x.total for x in containers["storage"]) 17 | pets_total = sum(x.total for x in containers["pets"]) 18 | 19 | purse = extras["purse"] 20 | banking = extras["banking"] 21 | 22 | return { 23 | "profile_data": profile_data, 24 | "purse": purse, 25 | "banking": banking, 26 | "inventory": inventory_total, 27 | "accessories": accessories_total, 28 | "equipment": equipment_total, 29 | "ender_chest": ender_chest_total, 30 | "armor": armour_total, 31 | "wardrobe": wardrobe_total, 32 | "vault": vault_total, 33 | "storage": storage_total, 34 | "pets": pets_total 35 | } 36 | 37 | -------------------------------------------------------------------------------- /api/endpoints/pages.py: -------------------------------------------------------------------------------- 1 | from data.container_handler import get_containers 2 | 3 | async def get_pages_dict(data, profile_data, uuid, profile_name): 4 | profile_data, containers, extras = get_containers(data, profile_data, uuid, profile_name) 5 | 6 | if profile_data is None: 7 | return None 8 | 9 | data = { 10 | "profile_data": profile_data, 11 | "purse": {"total": str(extras["purse"])}, 12 | "banking": {"total": str(extras["banking"])}, 13 | } 14 | 15 | 16 | for container in ("inventory", "accessories", "equipment", "ender_chest", "armor", "wardrobe", "vault", "storage", "pets"): 17 | top_x = sorted(containers[container], key=lambda x: x.total, reverse=True)[:5] 18 | prices = [x.to_dict() for x in top_x] 19 | data[container] = { 20 | "total": f"{sum(x.total for x in containers[container])}", 21 | "prices": prices, 22 | } 23 | 24 | return data 25 | -------------------------------------------------------------------------------- /api/endpoints/total.py: -------------------------------------------------------------------------------- 1 | from data.container_handler import get_containers 2 | 3 | async def get_total_value(data, profile_data, uuid, profile_name): 4 | profile_data, containers, extras = get_containers(data, profile_data, uuid, profile_name) 5 | 6 | if containers is None: 7 | return None 8 | 9 | total = sum([x for x in extras.values()]) 10 | 11 | for item_list in containers.values(): 12 | total += sum(x.total for x in item_list) 13 | 14 | return {"total": total} 15 | -------------------------------------------------------------------------------- /api/endpoints/tree.py: -------------------------------------------------------------------------------- 1 | from data.container_handler import get_containers 2 | 3 | async def get_tree(data, profile_data, uuid, profile_name): 4 | profile_data, containers, extras = get_containers(data, profile_data, uuid, profile_name) 5 | 6 | if containers is None: 7 | return None 8 | 9 | items = "" 10 | for container in ("inventory", "accessories", "equipment", "ender_chest", "armor", "wardrobe", "vault", "storage", "pets"): 11 | for price_object in containers[container]: 12 | item = price_object.to_dump_string() 13 | items += item+"\n" 14 | 15 | return {"data": items} 16 | -------------------------------------------------------------------------------- /api/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidProfileException(Exception): 2 | pass 3 | 4 | class NoProfilesException(Exception): 5 | pass 6 | 7 | class InvalidUUIDException(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /api/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UP929312/CommunityAPI/8be0f7ae060f132245d0180acbc5bf7c0b2bd84b/api/favicon.ico -------------------------------------------------------------------------------- /api/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, Body 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from fastapi.responses import JSONResponse, Response 4 | from fastapi_utils.tasks import repeat_every 5 | 6 | 7 | from endpoints.pages import get_pages_dict 8 | from endpoints.total import get_total_value 9 | from endpoints.groups import get_groups_value 10 | from endpoints.dump import get_dump_dict 11 | from endpoints.tree import get_tree 12 | 13 | from exceptions import InvalidProfileException, NoProfilesException, InvalidUUIDException 14 | from base_models import custom_body, default_response_types, PagesOut, TotalOut, GroupsOut, DumpOut, TreeOut 15 | 16 | from data.constants.collector import fetch_prices 17 | 18 | import uvicorn 19 | 20 | app = FastAPI() 21 | 22 | 23 | BAZAAR_CAPS = { 24 | "CARROT_ITEM": 26, "ENCHANTED_CARROT": 2999, "ENCHANTED_GOLDEN_CARROT": 421169, 25 | "POTATO_ITEM": 28, "ENCHANTED_POTATO": 2672, "ENCHANTED_BAKED_POTATO": 362046, 26 | "NETHER_STALK": 38, "ENCHANTED_NETHER_STALK": 4800, "MUTANT_NETHER_STALK": 767221, 27 | "WHEAT": 31, "ENCHANTED_HAY_BLOCK": 40882, "TIGHTLY_TIED_HAY_BALE": 4749995, 28 | "SUGAR_CANE": 22, "ENCHANTED_SUGAR_CANE": 543001, "ENCHANTED_SUGAR": 3299, 29 | "HAY_BLOCK": 984, 30 | } 31 | 32 | app.add_middleware( 33 | CORSMiddleware, 34 | allow_origins=["*"], 35 | allow_credentials=True, 36 | allow_methods=["*"], 37 | allow_headers=["*"], 38 | ) 39 | 40 | class Data: 41 | pass 42 | 43 | data = Data() 44 | 45 | @app.on_event("startup") 46 | @repeat_every(seconds=60*60, raise_exceptions=True) # 1 hour 47 | def update_price_lists_loop() -> None: 48 | print("Updating price lists loop") 49 | 50 | data.BAZAAR, data.LOWEST_BIN, data.PRICES, data.NPC_ITEMS = fetch_prices() 51 | data.BAZAAR["ENDER_PEARL"] = 100 52 | for item, price in BAZAAR_CAPS.items(): 53 | if data.BAZAAR[item] > price: 54 | data.BAZAAR[item] = price-price*0.25 55 | # For overrides 56 | for item, hard_price in [("RUNE", 5), ("WISHING_COMPASS", 1000), ("ICE_HUNK", 100),]: 57 | data.LOWEST_BIN[item] = hard_price 58 | # Price backups 59 | for item, hard_price in [("SCATHA;2", 250_000_000),("SCATHA;3", 500_000_000), ("SCATHA;4", 1_000_000_000 ), ("GAME_ANNIHILATOR", 2_500_000_000), ("GAME_BREAKER", 1_000_000_000), ]: 60 | if item not in data.LOWEST_BIN: 61 | data.LOWEST_BIN[item] = hard_price 62 | 63 | async def validate(function, params): 64 | try: 65 | returned_data = await function(*params) 66 | if isinstance(returned_data, dict): 67 | return JSONResponse(status_code=200, content=returned_data) 68 | 69 | print("ERROR!") 70 | return JSONResponse(status_code=500, content={"message": "An internal exception occured."}) 71 | 72 | except InvalidProfileException: 73 | return JSONResponse(status_code=401, content={"message": "Invalid profile given. That player hasn't got a profile with that name."}) 74 | except NoProfilesException: 75 | return JSONResponse(status_code=402, content={"message": "No profiles found for the given profile_data."}) 76 | except InvalidUUIDException: 77 | return JSONResponse(status_code=404, content={"message": "UUID couldn't be found on that profile."}) 78 | except: 79 | return JSONResponse(status_code=500, content={"message": "An internal exception occured."}) 80 | 81 | 82 | @app.post("/pages/{uuid}", response_model=PagesOut, responses=default_response_types) 83 | async def pages(request: Request, uuid: str, profile_data: custom_body, profile_name: str = None): # = Body(..., examples=pages_example_inputs) 84 | """ 85 | Returns each category's total, as well as the top 5 most expensive items from each catagory. 86 | 87 | - **uuid**: the player you want to target 88 | - **profile_name**: (optional) the profile you watch to target 89 | 90 | Request body:
91 | ⠀⠀⠀⠀The body needs to be a user's profile data, sent over in JSON format. It should be a jsonified version of the
92 | ⠀⠀⠀⠀response that is sent from https://api.hypixel.net/skyblock/profiles?key={api_key}&uuid={uuid} 93 | """ 94 | return await validate(get_pages_dict, (data, profile_data.profiles, uuid, profile_name)) 95 | 96 | 97 | @app.post("/total/{uuid}", response_model=TotalOut, responses=default_response_types) 98 | async def total(request: Request, uuid: str, profile_data: custom_body, profile_name: str = None): 99 | """ 100 | Returns the combined total including purse, banking and all inventories, 101 | with a single "total" field. 102 | 103 | - **uuid**: the player you want to target 104 | - **profile_name**: (optional) the profile you watch to target 105 | 106 | Request body:
107 | ⠀⠀⠀⠀The body needs to be a user's profile data, sent over in JSON format. It should be a jsonified version of the
108 | ⠀⠀⠀⠀response that is sent from https://api.hypixel.net/skyblock/profiles?key={api_key}&uuid={uuid} 109 | """ 110 | return await validate(get_total_value, (data, profile_data.profiles, uuid, profile_name)) 111 | 112 | 113 | @app.post("/groups/{uuid}", response_model=GroupsOut, responses=default_response_types) 114 | async def groups(request: Request, uuid: str, profile_data: custom_body, profile_name: str = None): 115 | """ 116 | Returns a map of all containers and their corresponding totals. 117 | 118 | - **uuid**: the player you want to target 119 | - **profile_name**: (optional) the profile you watch to target 120 | 121 | Request body:
122 | ⠀⠀⠀⠀The body needs to be a user's profile data, sent over in JSON format. It should be a jsonified version of the
123 | ⠀⠀⠀⠀response that is sent from https://api.hypixel.net/skyblock/profiles?key={api_key}&uuid={uuid} 124 | """ 125 | return await validate(get_groups_value, (data, profile_data.profiles, uuid, profile_name)) 126 | 127 | 128 | @app.post("/dump/{uuid}", response_model=PagesOut, responses=default_response_types) 129 | async def dump(request: Request, uuid: str, profile_data: custom_body, profile_name: str = None): 130 | """ 131 | Returns a complete dump off *all* item data, the prices and their parsed data. 132 | 133 | - **uuid**: the player you want to target 134 | - **profile_name**: (optional) the profile you watch to target 135 | """ 136 | return await validate(get_dump_dict, (data, profile_data.profiles, uuid, profile_name)) 137 | 138 | 139 | @app.post("/tree/{uuid}", response_model=TreeOut, responses=default_response_types, include_in_schema=False) 140 | async def tree(request: Request, uuid: str, profile_data: custom_body, profile_name: str = None): 141 | """ 142 | Returns a tree-like structure to aid in visualising the output data, 143 | returned with new line characters and calculated spacing. 144 | 145 | - **uuid**: the player you want to target 146 | - **profile_name**: (optional) the profile you watch to target 147 | 148 | Request body:
149 | ⠀⠀⠀⠀The body needs to be a user's profile data, sent over in JSON format. It should be a jsonified version of the
150 | ⠀⠀⠀⠀response that is sent from https://api.hypixel.net/skyblock/profiles?key={api_key}&uuid={uuid} 151 | """ 152 | return await validate(get_tree, (data, profile_data.profiles, uuid, profile_name)) 153 | 154 | 155 | @app.get("/", include_in_schema=False) 156 | async def root(request: Request): 157 | return JSONResponse(status_code=200, content={"message": "Hello world!"}) 158 | 159 | @app.get("/online") 160 | async def test_online(request: Request): 161 | """ 162 | A quick endpoint to test the status of the endpoint, should return a regular 200 status code. 163 | """ 164 | return JSONResponse(status_code=200, content={"message": "API Operational"}) 165 | 166 | # This is to just stop random bot on the internet from causing errors 167 | @app.get('/favicon.ico', include_in_schema=False) 168 | async def favicon(): 169 | return Response("favicon.ico", media_type="image/ico") 170 | 171 | # This is to just stop random bot on the internet from causing errors 172 | @app.head("/", include_in_schema=False) 173 | async def root_head(request: Request): 174 | return JSONResponse(status_code=200, content={"message": "Hello world!"}) 175 | 176 | 177 | if __name__ == "__main__": 178 | print("Done") 179 | uvicorn.run(app, host='0.0.0.0', port=8000, debug=True) 180 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.82.0 2 | fastapi_utils==0.2.1 3 | parameterized==0.8.1 4 | pydantic==1.8.2 5 | requests==2.25.1 6 | uvicorn==0.13.4 7 | -------------------------------------------------------------------------------- /api/zz check for.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | find = "NEW_USER" 4 | 5 | folders = [f.path for f in os.scandir(".") if f.is_dir()]+[""] 6 | 7 | def find_it(folder, ext): 8 | for file in folder: 9 | if file.endswith(".py") and file != "zz check for.py": 10 | with open(ext+file, "r", encoding="utf-8") as a: 11 | try: 12 | for line in a.readlines(): 13 | if find.lower() in line.lower(): 14 | print(" > Found in", file, " contained "+find) 15 | except Exception as e: 16 | print(e) 17 | pass 18 | 19 | for folder in folders: 20 | if folder in [".\__pycache__", ".\.mypy_cache"]: 21 | continue 22 | print("Looking in folder:", folder) 23 | if folder == "": 24 | find_it(os.listdir(), "") 25 | else: 26 | find_it(os.listdir(folder), folder+"/") 27 | -------------------------------------------------------------------------------- /bot/bot.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | 4 | import json # For loading the uuid conversion cache 5 | 6 | from database_manager import load_guild_prefix, load_prefixes, load_linked_accounts 7 | from utils import error as error_embed 8 | 9 | intents = discord.Intents(messages=True, guilds=True, emojis=True, message_content=True) 10 | 11 | print("1. Importing discord, json and other util packages done.") 12 | 13 | from networth.networth import networth_cog 14 | #from networth.guild_networth import guild_networth_cog 15 | 16 | print("2. Imported networth and guild networth done.") 17 | 18 | from player_commands import * 19 | 20 | print("3. Importing all player commands done.") 21 | 22 | LOCAL_BOT = True 23 | def get_prefix(bot: commands.Bot, msg: discord.Message): 24 | if LOCAL_BOT: return "!" 25 | prefix = bot.prefixes.get(f"{msg.guild.id}", ".") if msg.guild else "." 26 | return commands.when_mentioned_or(prefix)(bot, msg) 27 | 28 | client = commands.AutoShardedBot(command_prefix=get_prefix, help_command=None, case_insensitive=True, owner_id=244543752889303041, intents=intents, allowed_mentions=discord.AllowedMentions(everyone=False)) 29 | client.prefixes = dict(load_prefixes()) 30 | client.linked_accounts = dict(load_linked_accounts()) 31 | 32 | print("4. Client init done and data fetched") 33 | 34 | # Load in the stored uuid conversion cache for .leaderboard (they're stored as uuids) 35 | with open("text_files/uuid_conversion_cache.json", 'r') as file: 36 | client.uuid_conversion_cache = json.load(file) 37 | #==================================================== 38 | client.first_run = True 39 | 40 | @client.event 41 | async def on_ready() -> None: 42 | print("on_ready fired! =====") 43 | if client.first_run: 44 | client.first_run = False 45 | print("Done") # To tell the VM startup was complete 46 | print('Bot up and running.\nLoaded in on the community bot!') 47 | client.emoji_guild = await client.fetch_guild(860247551008440320) 48 | 49 | 50 | @client.event 51 | async def on_command_error(ctx, error) -> None: 52 | print("In on_command error:", str(error)) 53 | if isinstance(error, (commands.CommandNotFound, commands.errors.MissingAnyRole, discord.Forbidden)): 54 | pass 55 | elif isinstance(error, commands.errors.CheckFailure): 56 | return await ctx.respond("You're not allowed to do that here. Try a bot channel instead?", ephemeral=True) 57 | else: 58 | print(f"##### ERROR, The command was: {ctx.message.content}. It was done in {ctx.guild.name}, ({ctx.guild.id}) by {ctx.author.display_name} ({ctx.author.id})") 59 | print(str(error)) 60 | return await error_embed(ctx, "Error, something failed on our side.", f"The error that occured was: {error}, if this continues, please report it to Skezza#1139,") 61 | 62 | @client.event 63 | async def on_interaction(interaction) -> None: 64 | if interaction.type == discord.InteractionType.application_command: 65 | message = ", ".join([f"'{argument['name']}: {argument['value']}'" for argument in interaction.data.get('options', {})]) 66 | print(f"-- User {interaction.user.display_name} ({interaction.user.id}) performed `/{interaction.data['name']} {message}`\n"+ 67 | f"-- in {'DMs' if interaction.guild is None else interaction.guild.name} ({'DMs' if interaction.guild is None else interaction.guild.id}) - {'DMs' if interaction.guild is None else interaction.channel.name}") 68 | await client.process_application_commands(interaction) 69 | 70 | @client.event 71 | async def on_application_command_error(ctx, error): 72 | print(ctx, error) 73 | 74 | # For text commands 75 | @client.event 76 | async def on_command_completion(ctx) -> None: 77 | print(f"-- User {ctx.author.display_name} ({ctx.author.id}) performed `{ctx.message.content}`\n"+ 78 | f"-- in {'DMs' if ctx.guild is None else ctx.guild.name} ({ctx.guild.id if ctx.guild is not None else 'DMs'}) - {'DMs' if ctx.guild is None else ctx.channel.name}") 79 | #==================================================== 80 | 81 | print("5. Creating cogs list done.") 82 | all_cogs = [networth_cog,] 83 | #all_cogs.append(guild_networth_cog) 84 | all_cogs.extend(player_commands) 85 | 86 | for cog in all_cogs: 87 | client.add_cog(cog(client)) 88 | 89 | #from test_cog import test_cog 90 | #client.add_cog(test_cog(client)) 91 | 92 | print("6. Added cogs done.") 93 | 94 | #client.ip_address = "149.102.131.110" 95 | client.ip_address = "127.0.0.1" 96 | 97 | bot_key = open("text_files/bot_key.txt" if not LOCAL_BOT else "text_files/dev_bot_key.txt","r").read() 98 | client.run(bot_key) 99 | -------------------------------------------------------------------------------- /bot/database_manager.py: -------------------------------------------------------------------------------- 1 | import mysql.connector # type: ignore 2 | import datetime 3 | 4 | from typing import Union, Any, List, Optional, cast 5 | 6 | with open("text_files/database_creds.txt") as file: 7 | host, user, password, database = [x.rstrip("\n") for x in file.readlines()] 8 | 9 | #=========================================== 10 | def fetch_data(*args) -> list[tuple]: 11 | try: 12 | mydb = mysql.connector.connect(host=host, user=user, password=password, database=database, port=3306) 13 | cursor = mydb.cursor(*args) 14 | 15 | cursor.execute(*args) 16 | records = cursor.fetchall() 17 | except Exception as e: 18 | print("Database manager error, tried fetching but failed:", e) 19 | finally: 20 | cursor.close() 21 | 22 | return records 23 | 24 | def execute_command(*args) -> None: 25 | try: 26 | mydb = mysql.connector.connect(host=host, user=user, password=password, database=database, port=3306) 27 | cursor = mydb.cursor() 28 | 29 | cursor.execute(*args) 30 | mydb.commit() 31 | except Exception as e: 32 | print("Database manager error, tried executing but failed:", e) 33 | finally: 34 | cursor.close() 35 | 36 | #=========================================== 37 | # For settings prefixes 38 | def load_guild_prefix(guild_id: int) -> Optional[str]: 39 | records = fetch_data("SELECT prefix FROM guild_prefixes WHERE guild_id=%s", (guild_id,)) 40 | return (None if records == [] else records[0][0]) 41 | 42 | def set_guild_prefix(guild_id: int, prefix: str) -> None: 43 | execute_command("INSERT INTO guild_prefixes (guild_id, prefix) VALUES (%s, %s)", (guild_id, prefix)) 44 | 45 | def update_guild_prefix(guild_id: int, prefix: str) -> None: 46 | execute_command("UPDATE guild_prefixes SET prefix=%s WHERE guild_id=%s", (prefix, guild_id)) 47 | 48 | def load_prefixes() -> list[tuple]: 49 | return fetch_data("SELECT guild_id, prefix FROM guild_prefixes") 50 | 51 | #=========================================== 52 | # For linking accounts 53 | def load_linked_account(discord_id: int) -> Optional[str]: 54 | records = fetch_data("SELECT username FROM linked_accounts WHERE discord_id=%s", (discord_id,)) 55 | return (None if records == [] else records[0][0]) 56 | 57 | def set_linked_account(discord_id: int, username: str) -> None: 58 | execute_command("INSERT INTO linked_accounts (discord_id, username) VALUES (%s, %s)", (discord_id, username)) 59 | 60 | def update_linked_account(discord_id: int, username: str) -> None: 61 | execute_command("UPDATE linked_accounts SET username=%s WHERE discord_id=%s", (username, discord_id)) 62 | 63 | def load_linked_accounts() -> list[tuple]: 64 | return fetch_data("SELECT discord_id, username FROM linked_accounts") 65 | 66 | #=========================================== 67 | # For leaderboard and adding them 68 | def insert_profile(uuid, profile_name, profile_type, purse, banking, inventory, accessories, ender_chest, armor, vault, wardrobe, storage, pets) -> None: 69 | execute_command('''INSERT INTO stored_profiles (uuid, datetime, profile_name, profile_type, purse, banking, inventory, accessories, ender_chest, armor, vault, wardrobe, storage, pets) 70 | VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)''', 71 | (uuid, profile_name, profile_type, purse, banking, inventory, accessories, ender_chest, armor, vault, wardrobe, storage, pets)) 72 | 73 | def get_max_current_networth(profile_type: str="regular") -> list[tuple]: 74 | # WHERE profile_type = %s 75 | return fetch_data(''' 76 | SELECT t1.uuid, (t1.purse+t1.banking+t1.inventory+t1.accessories+t1.ender_chest+t1.armor+t1.vault+t1.wardrobe+t1.storage+t1.pets) AS total FROM 77 | stored_profiles AS t1 INNER JOIN ( 78 | SELECT t3.uuid, MAX(t3.datetime) AS datetime 79 | FROM stored_profiles AS t3 80 | WHERE profile_type = %s 81 | GROUP BY t3.uuid 82 | ) AS t2 ON t1.uuid = t2.uuid AND t1.datetime = t2.datetime 83 | ORDER BY total DESC LIMIT 100 84 | ''', (profile_type,)) 85 | 86 | #=========================================== 87 | # For rank 88 | def get_specific_networth_data(uuid: str) -> list[tuple]: 89 | return fetch_data(''' 90 | SELECT purse, banking, inventory, accessories, ender_chest, armor, vault, wardrobe, storage, pets 91 | FROM stored_profiles 92 | WHERE uuid = %s 93 | ORDER BY datetime DESC 94 | LIMIT 1 95 | ''', (uuid,)) 96 | 97 | def get_all_networth_data() -> list[tuple]: 98 | return fetch_data(''' 99 | SELECT t1.purse, t1.banking, t1.inventory, t1.accessories, t1.ender_chest, t1.armor, t1.vault, t1.wardrobe, t1.storage, t1.pets FROM 100 | stored_profiles AS t1 INNER JOIN ( 101 | SELECT t3.uuid, MAX(t3.datetime) AS datetime 102 | FROM stored_profiles AS t3 103 | GROUP BY t3.uuid 104 | ) AS t2 ON t1.uuid = t2.uuid AND t1.datetime = t2.datetime 105 | ''') 106 | 107 | def get_sum_networth_data() -> list[tuple]: 108 | return fetch_data(''' 109 | SELECT (t1.purse+t1.banking+t1.inventory+t1.accessories+t1.ender_chest+t1.armor+t1.vault+t1.wardrobe+t1.storage+t1.pets) FROM 110 | stored_profiles AS t1 INNER JOIN ( 111 | SELECT t3.uuid, MAX(t3.datetime) AS datetime 112 | FROM stored_profiles AS t3 113 | GROUP BY t3.uuid 114 | ) AS t2 ON t1.uuid = t2.uuid AND t1.datetime = t2.datetime 115 | ''') 116 | 117 | #records = fetch_data("SELECT * FROM stored_profiles") 118 | #print(records) 119 | #print(sum([x[0] for x in get_sum_networth_data()])) 120 | #=========================================== 121 | #print(get_max_current_networth()) 122 | -------------------------------------------------------------------------------- /bot/dev.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | OPTIONS = ["Paris", "London", "Tokyo", "New York", "Warsaw"] 5 | 6 | 7 | class StaticPresetMenuButton(discord.ui.Button): 8 | def __init__(self, label): 9 | super().__init__(style=discord.ButtonStyle.blurple, label=label) 10 | 11 | 12 | class StaticPresetMenuView(discord.ui.View): 13 | def __init__(self, options): 14 | super().__init__() 15 | self.options = options 16 | 17 | for option in options: 18 | self.add_item(StaticPresetMenuButton(label=option)) 19 | 20 | async def generate_static_preset_menu(ctx, embed, options): 21 | await ctx.send(embed=embed, view=StaticPresetMenuView(options=options)) 22 | 23 | 24 | class dev_cog(commands.Cog): 25 | def __init__(self, bot) -> None: 26 | self.client = bot 27 | 28 | @commands.command(name="dev") 29 | async def dev_command(self, ctx): 30 | embed = discord.Embed(title=f"Question number 5", description="In the year 987 A.D, currently with 67 million people, what city was made to be the capital of France?", colour=0x3498DB) 31 | embed.set_footer(text=f"Quiz hosted by @DrMatt") 32 | 33 | await generate_static_preset_menu(ctx, embed, OPTIONS) 34 | -------------------------------------------------------------------------------- /bot/emojis.py: -------------------------------------------------------------------------------- 1 | ITEM_RARITY = { 2 | "COMMON": "<:common:1035458869616443402>", 3 | "UNCOMMON": "<:uncommon:1035458914059304990>", 4 | "RARE": "<:rare:1035458894438350854>", 5 | "EPIC": "<:epic:1035458874662211584>", 6 | "LEGENDARY": "<:legendary:1035458884611096586>", 7 | "MYTHIC": "<:mythic:1035458891376513074>", 8 | "SUPREME": "<:supreme:1035458904299163648> ", # Don't think this is used anymore 9 | "SPECIAL": "<:special:1035458901895811085>", 10 | "VERY_SPECIAL": "<:very_special:869652064224030830>", 11 | "DIVINE": "<:divine:890223730909929522>", 12 | "UNKNOWN": "", 13 | } 14 | 15 | NUMBER_EMOJIS = [":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:", "<:ten:876112518072909864>"] 16 | 17 | DUNGEON_BOSS_EMOJIS = { 18 | "1": "<:bonzo:1035458864709124136>", 19 | "2": "<:scarf:1035458898406162502>", 20 | "3": "<:the_professor:1035458907511996466> ", 21 | "4": "<:thorn:1035458908766076969> ", 22 | "5": "<:livid:1035458886020374588>", 23 | "6": "<:sadan:1035458897286270986>", 24 | "7": "<:necron:1035458892001448038>", 25 | } 26 | 27 | MINION_TIER_EMOJIS = { 28 | 2: "<:t2_minion:872063121253097522>", 29 | 3: "<:t3_minion:872063101837672458>", 30 | 4: "<:t4_minion:872063093339983932>", 31 | 5: "<:t5_minion:872063084179619900>", 32 | 6: "<:t6_minion:872063074683732009>", 33 | 7: "<:t7_minion:872062768705077288>", 34 | 8: "<:t8_minion:872063053749948476>", 35 | 9: "<:t9_minion:872063039879380992>", 36 | 10: "<:t10_minion:1041798388309573633>", 37 | 11: "<:t11_minion:872063018282917929>", 38 | 12: "<:t12_minion:872063006639538176>", 39 | } 40 | 41 | MATHS_EMOJIS = { 42 | "min": "<:minimum:870299134454812672>", 43 | "max": "<:maximum:870299134349967420>", 44 | "volume": "<:volume:870301300083003423>", 45 | "median": "<:median:870304273429327903>", 46 | "mode": "<:mode:870304273806802955>", 47 | "mean": "<:mean:870299134467407902>", 48 | } 49 | 50 | SKILL_EMOJIS = { 51 | "farming": "<:farming:1035458875744329738>", 52 | "mining": "<:mining:1035458888658604123>", 53 | "combat": "<:combat:1035458868425273394>", 54 | "foraging": "<:foraging:1035458878386749470>", 55 | "fishing": "<:fishing:1035458876813877278>", 56 | "enchanting": "<:enchanting:1035458872867037196>", 57 | "alchemy": "<:alchemy:1035458858119860285>", 58 | "taming": "<:taming:1035458905406443560> ", 59 | "carpentry": "<:carpentry:1035458865589911574>", 60 | "runecrafting": "<:runecrafting:1035458896011198464>" 61 | } 62 | 63 | SLAYER_EMOJIS = { 64 | "zombie": "<:revenant:1035458916781408296>", 65 | "spider": "<:tarantula:1035458903258976306>", 66 | "wolf": "<:sven:1035458915204345866>", 67 | "enderman": "<:voidgloom:867330759073464360>" 68 | } 69 | 70 | CLASS_EMOJIS = { 71 | "catacombs": "<:catacombs:864618274900410408>", 72 | "healer": "<:healer:1035458883579289620>", 73 | "mage": "<:mage:1035458887500959764>", 74 | "berserk": "<:berserker:1035458862570029076>", 75 | "archer": "<:archer:1035458859113914392>", 76 | "tank": "<:tank:1035458906417274890>", 77 | } 78 | 79 | # Only for use on `rank`, not in networth 80 | PAGE_ICON_EMOJIS = { 81 | "overall": "<:paper:1035458893482037278>", 82 | 83 | "banking": "<:banking:1035458860556759100>", 84 | "purse": "<:main:854797453223657505>", 85 | "inventory": "<:inventory:854797467726643210>", 86 | "ender chest": "<:ender_chest:854797443321036830>", 87 | "accessories": "<:accessories:854797427420823572>", 88 | "wardrobe": "<:wardrobe:854797516078972928>", 89 | 90 | "storage": "<:storage:854797494830628884>", 91 | "vault": "<:vault:854841046151331900>", 92 | "armor": "<:armor:855021791391383562>", 93 | "pets": "<:pets:854797481132032090>", 94 | } 95 | -------------------------------------------------------------------------------- /bot/extract_ids.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | from gzip import decompress 3 | from io import BytesIO 4 | from struct import unpack 5 | 6 | from typing import Union 7 | 8 | def parse_container(raw: str) -> list: 9 | """ 10 | This will decompress and decode the base64 data that Hypixel returns from the API. 11 | This will then get parsed into one of the other functions below 12 | """ 13 | raw_bytes = BytesIO(decompress(b64decode(raw))) # Unzip raw string from the api 14 | 15 | def read(type, length): 16 | if type in 'chil': 17 | return int.from_bytes(raw_bytes.read(length), byteorder='big') 18 | if type == 's': 19 | return raw_bytes.read(length).decode('utf-8') 20 | return unpack('>' + type, raw_bytes.read(length))[0] 21 | 22 | def parse_list(): 23 | subtype = read('c', 1) 24 | payload = [] 25 | for _ in range(read('i', 4)): 26 | parse_next_tag(payload, subtype) 27 | return payload 28 | 29 | def parse_compound(): 30 | payload = {} 31 | while parse_next_tag(payload) != 0: # Parse tags until we find an endcap (type == 0) 32 | pass # Nothing needs to happen here 33 | return payload 34 | 35 | payloads = { 36 | 1: lambda: read('c', 1), # Byte 37 | 2: lambda: read('h', 2), # Short 38 | 3: lambda: read('i', 4), # Int 39 | 4: lambda: read('l', 8), # Long 40 | 5: lambda: read('f', 4), # Float 41 | 6: lambda: read('d', 8), # Double 42 | 7: lambda: raw_bytes.read(read('i', 4)), # Byte Array 43 | 8: lambda: read('s', read('h', 2)), # String 44 | 9: parse_list, # List 45 | 10: parse_compound, # Compound 46 | 11: lambda: [read('i', 4) for _ in range(read('i', 4))], # Int Array 47 | 12: lambda: [read('l', 8) for _ in range(read('i', 4))] # Long Array 48 | } 49 | 50 | def parse_next_tag(dictionary, tag_id=None): 51 | if tag_id is None: # Are we inside a list? 52 | tag_id = read('c', 1) 53 | if tag_id == 0: # Is this the end of a compound? 54 | return 0 55 | name = read('s', read('h', 2)) 56 | 57 | payload = payloads[tag_id]() 58 | if isinstance(dictionary, dict): 59 | dictionary[name] = payload 60 | else: 61 | dictionary.append(payload) 62 | 63 | raw_bytes.read(3) # Remove file header (we ingore footer) 64 | root: dict = {} 65 | parse_next_tag(root) 66 | if not root: 67 | return [] 68 | return [x for x in root['i'] if x] 69 | 70 | def extract_internal_id(nbt: dict) -> str: 71 | """ 72 | Takes the data from the decode container function and returns 73 | the internal_id 74 | """ 75 | tag = nbt.get('tag', {}) 76 | internal_name = tag.get('ExtraAttributes', {"id": "UNKNOWN"}).get('id', "UNKNOWN") 77 | 78 | return internal_name 79 | 80 | def extract_nbt_dicts(raw: str) -> list[dict]: 81 | """ 82 | Takes a raw compressed encoded string and returns all the items in that 83 | list in an nbt dictionary 84 | """ 85 | return [x['tag'] for x in parse_container(raw) if 'tag' in x.keys()] 86 | 87 | def extract_internal_names(raw: str) -> list[str]: 88 | """ 89 | Extracts all the internal names from a container 90 | """ 91 | return [extract_internal_id(x) for x in parse_container(raw)] 92 | 93 | 94 | -------------------------------------------------------------------------------- /bot/networth/constants.py: -------------------------------------------------------------------------------- 1 | page_names = ["main", "inventory", "accessories", "ender_chest", "armor", "equipment", "wardrobe", "storage", "pets", "misc"] 2 | 3 | # Item descriptive icons 4 | 5 | MISSING = "<:missing:854823285825208372>" 6 | 7 | PRICE_SOURCE = "<:price_source:854752333299974174>" 8 | RECOMBOBULATOR = "<:recombobulator:854750106376339477>" 9 | ART_OF_WAR = "<:art_of_war:854750132721811466>" 10 | HOT_POTATO_BOOK = "<:hot_potatos:854753109305065482>" 11 | TALISMAN_ENRICHMENT = "<:talisman_enrichment:855013601232551966>" 12 | ENCHANTMENTS = "<:enchantments:854756289010728970>" 13 | REGULAR_STARS = "<:regular_stars:854752631741480990>" 14 | MASTER_STARS = "<:master_stars:854750116066230323>" 15 | SKIN = "<:armor_skin:867793856245006358>" 16 | POWER_ABILITY_SCROLL = "<:power_ability_scroll:1035458856949653504>" 17 | GEMS = "<:gemstone:1035458879523393557>" 18 | GEMSTONE_CHAMBERS = "<:gemstone_chamber:1041792988319330305>" 19 | GEMSTONE_POWER_SCROLL = "<:gemstone_power_scroll:1035458882425847838>" 20 | REFORGE = "<:reforge:854750152048246824>" 21 | TRANSMISSIONS = "<:transmission_tuner:856491122736758814>" 22 | ETHERMERGE = "<:ethermerge:856616140246351892>" 23 | WINNING_BID = "<:winning_bid:856491169750712320>" 24 | DYE = "<:dye:1035458871742959648>" 25 | 26 | PET_ITEM = "<:pet_item:854768366014169129>" 27 | PET_SKIN = "<:pet_skin:854768054424305684>" 28 | LEVEL = "<:level:854767687623639080>" 29 | 30 | # PAGES ICONS 31 | 32 | MAIN = "<:main:854797453223657505>" 33 | INVENTORY = "<:inventory:854797467726643210>" 34 | ENDER_CHEST = "<:ender_chest:854797443321036830>" 35 | ACCESSORES = "<:belt:1035458861596938270>" 36 | WARDROBE = "<:wardrobe:854797516078972928>" 37 | 38 | STORAGE = "<:storage:854797494830628884>" 39 | #VAULT = "<:vault:854841046151331900>" 40 | EQUIPMENT = "<:belt:985209099354538014>" 41 | ARMOUR = "<:armor:855021791391383562>" 42 | PETS = "<:pets:854797481132032090>" 43 | MISC = "<:misc:854801277489774613>" 44 | 45 | PAGE_TO_EMOJI: dict[str, str] = { 46 | "main": MAIN, 47 | "inventory": INVENTORY, 48 | "ender_chest": ENDER_CHEST, 49 | "accessories": ACCESSORES, 50 | "wardrobe": WARDROBE, 51 | "storage": STORAGE, 52 | "equipment": EQUIPMENT, 53 | "armor": ARMOUR, 54 | "pets": PETS, 55 | "misc": MISC, 56 | } 57 | 58 | PAGES = list(PAGE_TO_EMOJI.keys()) 59 | EMOJI_LIST = list(PAGE_TO_EMOJI.values()) 60 | 61 | PAGE_TO_IMAGE = { 62 | "main": "https://cdn.discordapp.com/emojis/854797453223657505.png?v=1", 63 | "inventory": "https://cdn.discordapp.com/emojis/854797467726643210.png?v=1", 64 | "ender_chest": "https://cdn.discordapp.com/emojis/854797443321036830.png?v=1", 65 | "accessories": "https://cdn.discordapp.com/emojis/854797427420823572.png?v=1", 66 | "wardrobe": "https://cdn.discordapp.com/emojis/854797516078972928.png?v=1", 67 | 68 | "storage": "https://cdn.discordapp.com/emojis/854797494830628884.png?v=1", 69 | #"vault": "https://cdn.discordapp.com/emojis/854841046151331900.png?v=1", 70 | "equipment": "https://cdn.discordapp.com/emojis/985209099354538014.png?v=1", 71 | "armor": "https://cdn.discordapp.com/emojis/855021791391383562.png?v=1", 72 | "pets": "https://cdn.discordapp.com/emojis/854797481132032090.png?v=1", 73 | "misc": "https://cdn.discordapp.com/emojis/854801277489774613.png?v=1", 74 | } 75 | -------------------------------------------------------------------------------- /bot/networth/generate_description.py: -------------------------------------------------------------------------------- 1 | from utils import hf, clean 2 | from networth.constants import PRICE_SOURCE, RECOMBOBULATOR, ART_OF_WAR, HOT_POTATO_BOOK, TALISMAN_ENRICHMENT, ENCHANTMENTS, REGULAR_STARS, MASTER_STARS, SKIN, POWER_ABILITY_SCROLL, GEMS, GEMSTONE_CHAMBERS, REFORGE, TRANSMISSIONS, ETHERMERGE, WINNING_BID, DYE, PET_ITEM, PET_SKIN, LEVEL 3 | 4 | def generate_item_description(v: dict) -> str: 5 | elems = [] 6 | #elems.append(f"{BASE_PRICE} - Base cost: {v['base_price']}") 7 | elems.append(f"{PRICE_SOURCE} - Price source: {v['price_source']}") 8 | if "recombobulator_value" in v: 9 | elems.append(f"{RECOMBOBULATOR} - Recombobulator: +{hf(v['recombobulator_value'])}") 10 | if "art_of_war_value" in v: 11 | elems.append(f"{ART_OF_WAR} - Art of War: +{hf(v['art_of_war_value'])}") 12 | if "hot_potatoes" in v: 13 | potato_books = v['hot_potatoes']['hot_potato_books']+v['hot_potatoes'].get("fuming_potato_books", 0) 14 | elems.append(f"{HOT_POTATO_BOOK} - Potato books: +{hf(potato_books)}") 15 | if "talisman_enrichment" in v: 16 | enrichment_item, enrichment_value = list(v['talisman_enrichment'].items())[0] 17 | elems.append(f"{TALISMAN_ENRICHMENT} - Enrichment: ({clean(enrichment_item)} - {hf(enrichment_value)})") 18 | if "enchantments" in v: 19 | elems.append(f"{ENCHANTMENTS} - Enchantments: +{hf(sum(v['enchantments'].values()))}") 20 | if "stars" in v: # This can sometimes be {} 21 | stars = v["stars"] 22 | if "regular_stars" in stars: 23 | elems.append(f"{REGULAR_STARS} - Regular stars: +{hf(stars['regular_stars']['total_essence_value'])}") 24 | if "master_stars" in stars: 25 | elems.append(f"{MASTER_STARS} - Master stars: ({len(stars['master_stars'])} stars - {hf(sum(stars['master_stars'].values()))})") 26 | if "skin" in v: 27 | skin_item, skin_value = list(v['skin'].items())[0] 28 | elems.append(f"{SKIN} - Skin: ({clean(skin_item)} - {hf(skin_value)})") 29 | if "power_ability_scroll" in v: 30 | power_ability_scroll_item, power_ability_scroll_value = list(v["power_ability_scroll"].items())[0] 31 | elems.append(f"{POWER_ABILITY_SCROLL} - Power scroll: ({clean(power_ability_scroll_item)} - {hf(power_ability_scroll_value)})") 32 | if "gems" in v: 33 | elems.append(f"{GEMS} - Gems: +{hf(sum(v['gems'].values()))}") 34 | if "gemstone_chambers" in v: 35 | elems.append(f"{GEMSTONE_CHAMBERS} - Gemstone chambers: +{hf(v['gemstone_chambers'])}") 36 | if "reforge" in v and v["reforge"]["apply_cost"] != 0: 37 | reforge_item, reforge_item_cost = list(v['reforge']['item'].items())[0] 38 | elems.append(f"{REFORGE} - Reforge: ({clean(reforge_item)} - {hf(reforge_item_cost)})") 39 | if "tuned_transmission" in v: 40 | elems.append(f"{TRANSMISSIONS} - Tuned transmissions: +{hf(v['tuned_transmission'])}") 41 | if "ethermerge" in v: 42 | elems.append(f"{ETHERMERGE} - Ethermerge: +{hf(v['ethermerge'])}") 43 | if "winning_bid" in v: 44 | elems.append(f"{WINNING_BID} - Winning bid: +{hf(v['winning_bid'])}") 45 | if "dye" in v: 46 | dye_item, dye_value = list(v["dye"].items())[0] 47 | elems.append(f"{DYE} - Dye: ({clean(dye_item)} - {hf(dye_value)})") 48 | 49 | return "\n".join(elems) 50 | 51 | def generate_pet_description(value: dict, item: dict) -> str: 52 | elems = [] 53 | v = value 54 | elems.append(f"{PRICE_SOURCE} - Price source: {v['price_source']}") 55 | if "held_item" in v: 56 | pet_item_formatted = item['heldItem'].removeprefix("PET_ITEM_") 57 | for rarity in ["_COMMON", "_UNCOMMON", "_RARE", "_EPIC", "_LEGENDARY", "_MYTHIC", "_SPECIAL", "_VERY_SPECIAL"]: 58 | pet_item_formatted = pet_item_formatted.removesuffix(rarity) 59 | elems.append(f"{PET_ITEM} - Pet item: ({clean(pet_item_formatted)} - {hf(v['held_item']['value'])})") 60 | if "pet_skin" in v: 61 | elems.append(f"{PET_SKIN} - Pet skin: ({clean(item['skin'])} - {hf(v['pet_skin']['value'])})") 62 | if "pet_level_bonus" in v: 63 | elems.append(f"{LEVEL} - Pet level bonus: {hf(v['pet_level_bonus']['worth'])}") 64 | return "\n".join(elems) 65 | 66 | def generate_description(value: dict, item: dict) -> str: 67 | if "candyUsed" in item: # For pets only 68 | return generate_pet_description(value, item) 69 | else: 70 | return generate_item_description(value) 71 | -------------------------------------------------------------------------------- /bot/networth/generate_page.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from typing import Any 3 | 4 | from utils import hf, clean 5 | from networth.generate_description import generate_description 6 | from networth.constants import page_names, PAGE_TO_IMAGE, PAGE_TO_EMOJI 7 | 8 | def format_info(total: int, item: dict, value: dict) -> str: 9 | name = item['name'] if 'name' in item else f"[Lvl {value['pet_level']}] {clean(item['tier'])} {clean(item['type'])}" 10 | reforge = "" if "reforge" not in item else clean(item['reforge'])+" " 11 | return_string = f"{reforge}{name} ➜ {hf(total)}" 12 | return return_string 13 | 14 | def generate_page(command_author: discord.Member, data: dict, username: str, page: str, use_guilds: bool=False) -> discord.Embed: 15 | 16 | embed = discord.Embed(colour=0x3498DB) 17 | 18 | # MAIN MENU 19 | if page == "main": 20 | total: str = hf(sum([float(x["total"]) for x in data.values() if "total" in x])) 21 | 22 | purse = float(data['purse']['total']) 23 | bank = float(data['banking']['total']) 24 | embed.add_field(name="**Purse**", value=f"{hf(purse)}", inline=True) 25 | embed.add_field(name="**Bank**", value=f"{hf(bank)}", inline=True) 26 | embed.add_field(name="**Combined**", value=f"{hf(purse+bank)}", inline=True) 27 | 28 | for page_string in page_names[1:-1]: # Remove purse and banking 29 | if data[page_string]["total"] == "0": 30 | continue 31 | page_total: str = data[page_string]["total"] 32 | top_x: list[dict] = data[page_string]["prices"] 33 | 34 | value: list[str] = [format_info(x['total'], x['item'], x['value']) for x in top_x] 35 | embed.add_field(name=f"**{PAGE_TO_EMOJI[page_string]} {clean(page_string)} ➜ {hf(data[page_string]['total'])}:**", value="\n".join(value), inline=False) 36 | 37 | # MISC 38 | elif page == "misc": 39 | embed.set_author(icon_url=PAGE_TO_IMAGE[page], name=f"Networth Command Limitations") 40 | embed.add_field(name="**1**: The value doesn't include minions", value="As minions aren't part of the API, they're not visible to the bot, so aren't included. However, these scale linearly (when compared to total network), and shouldn't make too much of a difference (relative wise).", inline=False) 41 | embed.add_field(name="**2**: The value doesn't include chests", value="Similarly, chests also aren't visible, and also aren't included, and while this is partially problematic, is impossible to add at the current time. The effect this has should be relatively minor.", inline=False) 42 | embed.add_field(name="**3**: Some values are subjective", value="While almost all items calculated have set prices (mostly from the auction house's BIN/Bazaar), one constant has been used: the value of Pet levels (e.g. Level 2 pet being worth more than a Level 1 pet), currently 0.2*xp (capped at Level 100).", inline=False) 43 | # All the rest 44 | else: 45 | total = hf(data[page]["total"]) 46 | top_x = data[page]["prices"] 47 | 48 | if top_x == []: # For disabled APIs or empty containers 49 | total = "" 50 | embed.add_field(name=f"{username} doesn't have any items here.", value="Perhaps they disabled their API?", inline=False) 51 | 52 | for price_object in top_x: 53 | item: dict = price_object["item"] 54 | value: str = generate_description(price_object["value"], item) 55 | 56 | if "candyUsed" in item: # For pets only 57 | embed.add_field(name=f"Level {price_object['value']['pet_level']} {clean(item['type'])} ➜ {hf(price_object['total'])}", value=value, inline=False) 58 | else: 59 | name = f"{clean(item.get('reforge', ''))} {item['name']}" 60 | embed.add_field(name=f"{name} ➜ {hf(price_object['total'])}", value=value, inline=False) 61 | 62 | if page != "misc": 63 | url = "https://media.discordapp.net/attachments/854829960974565396/871427090560462858/270px-BL-icon-banner-Guild_Banner_03.png" if use_guilds else f"https://api.hypixelskyblock.de/api/v1/cb/display/{username}" 64 | embed.set_author(icon_url=PAGE_TO_IMAGE[page], name=f"{username}'s {clean(page)} Networth - {total}", url=url) 65 | 66 | if not use_guilds: 67 | embed.set_thumbnail(url=f"https://cravatar.eu/helmhead/{username}") 68 | 69 | embed.set_footer(text=f" Command executed by {command_author.display_name} | Community Bot. By the community, for the community.") 70 | return embed 71 | 72 | -------------------------------------------------------------------------------- /bot/networth/guild_networth.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | 4 | from typing import Optional 5 | 6 | import aiohttp 7 | import requests 8 | 9 | from utils import error, bot_can_send 10 | from menus import generate_static_preset_menu 11 | from parse_profile import input_to_uuid 12 | from networth.generate_page import generate_page 13 | from networth.constants import PAGES, EMOJI_LIST 14 | 15 | with open('text_files/hypixel_api_key.txt') as file: 16 | API_KEY = file.read() 17 | 18 | async def fetch_all_users_data(self, uuid_list: list[str]) -> list[dict]: 19 | responses = [] 20 | async with aiohttp.ClientSession() as session: 21 | for uuid in uuid_list: 22 | async with session.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}") as resp: 23 | profile_data = await resp.json() 24 | async with session.post(f"http://{self.client.ip_address}:8000/pages/{uuid}", json=profile_data) as resp: 25 | responses.append(await resp.json()) 26 | return responses 27 | 28 | class guild_networth_cog(commands.Cog): 29 | def __init__(self, bot) -> None: 30 | self.client = bot 31 | 32 | @commands.command(aliases=["gnw", "gn"]) 33 | async def guild_networth(self, ctx: commands.Context, provided_username: Optional[str] = None) -> None: 34 | username, uuid = await input_to_uuid(ctx, provided_username, is_response=False) 35 | if uuid is None: 36 | return 37 | 38 | response = requests.get(f"https://api.hypixel.net/guild?key={API_KEY}&player={uuid}").json() 39 | 40 | members_uuid = [x['uuid'] for x in response['guild']['members']] 41 | guild_name = response['guild']['name'] 42 | 43 | responses = await fetch_all_users_data(self, members_uuid) 44 | 45 | all_responses, master_response = {}, {} 46 | master_response["purse"] = {"total": sum(int(x["purse"]["total"]) for x in responses)} 47 | master_response["banking"] = {"total": sum(int(x["banking"]["total"]) for x in responses)} 48 | 49 | for key in ("inventory", "accessories", "ender_chest", "armor", "wardrobe", "vault", "storage", "pets"): 50 | all_responses[key] = {} 51 | all_responses[key]["prices"] = [] 52 | for response in responses: 53 | all_responses[key]["prices"].extend(response[key]["prices"]) 54 | 55 | for key in ("inventory", "accessories", "ender_chest", "armor", "wardrobe", "vault", "storage", "pets"): 56 | master_response[key] = {} 57 | master_response[key]["prices"] = sorted(all_responses[key]["prices"], key=lambda x: x["total"], reverse=True)[:5] 58 | master_response[key]["total"] = str(sum([x["total"] for x in all_responses[key]["prices"]])) 59 | 60 | # Generate all the pages and initiate the menu handler 61 | list_of_embeds = [generate_page(command_author=ctx.author, data=master_response, username=guild_name, page=page, use_guilds=True) for page in PAGES] 62 | await generate_static_preset_menu(ctx=ctx, list_of_embeds=list_of_embeds, emoji_list=EMOJI_LIST, alternate_colours=True) 63 | -------------------------------------------------------------------------------- /bot/networth/networth.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | 8 | from utils import error, PROFILE_NAMES, API_KEY, bot_can_send, guild_ids 9 | from menus import generate_static_preset_menu 10 | from database_manager import insert_profile 11 | from parse_profile import get_profile_data 12 | from networth.generate_page import generate_page 13 | from networth.constants import PAGES, EMOJI_LIST 14 | 15 | 16 | class networth_cog(commands.Cog): 17 | def __init__(self, bot) -> None: 18 | self.client = bot 19 | 20 | @commands.command(name="networth", aliases=["nw", "n", "net", "worth", "now", "new"]) 21 | async def networth_command(self, ctx: commands.Context, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None) -> None: 22 | await self.get_networth(ctx, provided_username, provided_profile_name, is_response=False) 23 | 24 | @commands.slash_command(name="networth", description="Gets networth data about someone", guild_ids=guild_ids) 25 | async def networth_slash(self, ctx, username: Option(str, "username:", required=False), 26 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 27 | if not bot_can_send(ctx): 28 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 29 | await ctx.defer() 30 | await self.get_networth(ctx, username, profile, is_response=True) 31 | 32 | @commands.slash_command(name="nw", description="Alias of /networth", guild_ids=guild_ids) 33 | async def alias_networth_slash(self, ctx, username: Option(str, "username:", required=False), 34 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 35 | if not bot_can_send(ctx): 36 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 37 | await ctx.defer() 38 | await self.get_networth(ctx, username, profile, is_response=True) 39 | 40 | @commands.user_command(name="Get networth", guild_ids=guild_ids) 41 | async def networth_context_menu(self, ctx, member: discord.Member): 42 | if not bot_can_send(ctx): 43 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 44 | await self.get_networth(ctx, member.display_name, None, is_response=True) 45 | 46 | #================================================================================================================================ 47 | 48 | async def get_networth(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool = False) -> None: 49 | # Convert username/linked_account/nick to profile and more 50 | player_data = await get_profile_data(ctx, provided_username, provided_profile_name, return_profile_list=True, is_response=is_response) 51 | if player_data is None: 52 | return None 53 | username, uuid, profile_data, profile_name = player_data["data"] 54 | 55 | #======================= 56 | # Make the API request 57 | try: 58 | request = requests.post(f"http://{self.client.ip_address}:8000/pages/{uuid}?profile_name={profile_name}", json=profile_data) 59 | except Exception as e: 60 | print(e) 61 | return await error(ctx, "Error, the bot could not connect to the API", "This could be because the API is down for maintenance, because it's restarting, or because there are issues. Try again later.", is_response) 62 | #======================= 63 | # Deal with exceptions 64 | if request.status_code == 500: 65 | return await error(ctx, "Error, an exception has occured", "This happened internally. If it's continues, let the lead dev know (Skezza#1139)", is_response) 66 | elif request.status_code == 401: 67 | return await error(ctx, "Error, invalid profile given!", "Make sure it's one of their active profiles and try again.", is_response) 68 | elif request.status_code == 423: 69 | return await error(ctx, "Error, rate limit hit", "Your request has not been fufiled, please slow down and try again later.", is_response) 70 | elif request.status_code == 404: 71 | return await error(ctx, "Error, that person could not be found", "Perhaps you input the incorrect name?", is_response) 72 | #======================= 73 | data = request.json() 74 | 75 | # Generate all the pages and initiate the menu handler 76 | list_of_embeds = [generate_page(ctx.author, data, username, page) for page in PAGES] 77 | await generate_static_preset_menu(ctx=ctx, list_of_embeds=list_of_embeds, emoji_list=EMOJI_LIST, alternate_colours=True, is_response=is_response) 78 | 79 | # Add the data to the database (for the leaderboard command) 80 | data_totals: list[int] = [data[page]['total'] for page in ("purse", "banking", "inventory", "accessories", "ender_chest", "armor", "vault", "wardrobe", "storage", "pets")] 81 | insert_profile(uuid, data["profile_data"]["profile_name"], data["profile_data"]["profile_type"], *data_totals) 82 | -------------------------------------------------------------------------------- /bot/parse_profile.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from utils import error, PROFILE_NAMES 4 | from discord.ext import commands # type: ignore 5 | 6 | from typing import Union, Optional 7 | 8 | with open("text_files/hypixel_api_key.txt") as file: 9 | API_KEY = file.read() 10 | 11 | ALLOWED_CHARS = {"_", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} 12 | 13 | async def input_to_uuid(ctx, provided_username: Optional[str], is_response: bool) -> Optional[tuple[str, str]]: 14 | """ 15 | This will take a already given username, but if one isn't given, it will 16 | first check if they've linked their account, if not, it will try 17 | parsing their ign from their discord nicks, by trimming off their tag, 18 | e.g. '[Admin] Notch' will get parsed as 'Notch'. 19 | """ 20 | nick = False # Used to detect if we fell back on parsing nick 21 | if provided_username is None: # If no username is given 22 | if (linked_account := ctx.bot.linked_accounts.get(f"{ctx.author.id}")): # Check if they've linked their account 23 | username = linked_account 24 | else: # If not, parse their nickname 25 | username = ctx.author.display_name 26 | nick = True 27 | else: 28 | username = provided_username 29 | 30 | # Remove tags and wrong chars 31 | username = username.split("]")[1] if "]" in username else username 32 | username = "".join([char for char in username if char.lower() in ALLOWED_CHARS]) 33 | 34 | # If it's a username, get their uuid 35 | if len(username) <= 16: 36 | uuid_request = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{username}") 37 | if uuid_request.status_code > 200: 38 | return await error(ctx, "Error! Username was incorrect.", "Make sure you spelled the target player's name correctly.", is_response=is_response) 39 | 40 | # When we can't find a username, request.text will be '', if we can, it'll be the json string 41 | if not uuid_request.text: 42 | if not nick: 43 | return await error(ctx, "Error! Username was incorrect.", "Make sure you spelled the target player's name correctly.", is_response=is_response) 44 | else: 45 | return await error(ctx, "Error, could not parse username from discord nickname.", "No linked account was found through /link_account, and no username was given. Please link your account or provide a username", is_response=is_response) 46 | 47 | uuid = uuid_request.json()["id"] 48 | else: 49 | # if it's a uuid 50 | uuid = username 51 | username_request = requests.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}") 52 | if not username_request.text: 53 | return await error(ctx, "Error! UUID was incorrect.", "Could not find that player's uuid.", is_response=is_response) 54 | username_json = username_request.json() 55 | if isinstance(username_json, dict) and "error" in username_json.keys(): 56 | return await error(ctx, "Error! Input was invalid.", "Could not find that player!. Did you try pinging them? (Pings are annoying to users so won't be accepted)", is_response=is_response) 57 | 58 | username = username_request.json()["name"] 59 | 60 | return username, uuid 61 | 62 | async def get_profile_data(ctx: commands.Context, username: Optional[str], profile_provided: Optional[str] = None, return_profile_list: bool = False, is_response: bool = False) -> Optional[dict]: 63 | """ 64 | This will take a username, or None, and return a dictionary with 65 | Their profile data, with a few extra bits for convenience 66 | """ 67 | # If they want to use auto-name and give a profile 68 | if username is not None and username.lower() in PROFILE_NAMES: 69 | profile_provided = username 70 | username = None 71 | # Convert username/linked_account/nick to uuid 72 | data = await input_to_uuid(ctx, username, is_response) 73 | if data is None: 74 | return None 75 | username, uuid = data 76 | 77 | ################################# 78 | # Fetch profile from hypixel's API with uuid 79 | try: 80 | profile_list = requests.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}").json() 81 | except: 82 | return await error(ctx, "Error, the Hypixel API is in maintenance mode!", "Please try again in a few hours!", is_response=is_response) 83 | print("AHHHH, the Hypixel API isn't working properly!") 84 | 85 | 86 | if profile_list == {'success': False, 'cause': 'Invalid API key'}: 87 | print("############################### Error, key has failed again!") 88 | return await error(ctx, "Error, the api key used to run this bot has failed.", "This is because Hypixel randomly kill API keys. Please be patient, a fix is coming soon!", is_response=is_response) 89 | 90 | # profiles can be None, or not exist as key 91 | if profile_list is None or profile_list.get('profiles') is None: # If we can't find any profiles, they never made one 92 | return await error(ctx, "That user has never joined Skyblock before!", "Make sure you typed the name correctly and try again.", is_response=is_response) 93 | 94 | # For networth only 95 | if return_profile_list: 96 | return {"data": (username, uuid, profile_list, profile_provided)} 97 | 98 | ################################# 99 | # Either try find the given profile or use the latest joined 100 | if profile_provided is not None: # If they gave their own profile 101 | if not (profiles := [x for x in profile_list["profiles"] if x["cute_name"].lower() == profile_provided.lower()]): 102 | return await error(ctx, "Error, couldn't find that profile name", "Make sure you type it correctly and try again.", is_response=is_response) 103 | profile = profiles[0] 104 | else: # If they leave it for auto 105 | if not (valid_profiles := [x for x in profile_list["profiles"] if uuid in x['members'] and "selected" in x]): 106 | return await error(ctx, "Error, cannot find profiles for this user!", "Make sure you spelled the target player's name correctly", is_response=is_response) 107 | 108 | profile = max(valid_profiles, key=lambda x: x['selected']) 109 | 110 | ################################# 111 | # Save the profile data and some other bits because they're handy 112 | profile_dict = profile["members"][uuid] 113 | 114 | profile_dict["uuid"] = uuid 115 | profile_dict["username"]= username 116 | profile_dict["profile_id"] = profile["profile_id"] 117 | profile_dict["cute_name"] = profile["cute_name"] 118 | 119 | return profile_dict 120 | 121 | -------------------------------------------------------------------------------- /bot/player_commands/__init__.py: -------------------------------------------------------------------------------- 1 | from player_commands.bazaar import bazaar_cog 2 | from player_commands.sky import sky_cog 3 | from player_commands.wiki import wiki_cog 4 | from player_commands.dungeons import dungeons_cog 5 | from player_commands.kills import kills_cog 6 | from player_commands.lowest_bin import lowest_bin_cog 7 | from player_commands.skills import skills_cog 8 | from player_commands.slayer import slayer_cog 9 | from player_commands.invite import invite_cog 10 | from player_commands.auction_house import auction_house_cog 11 | from player_commands.missing import missing_cog 12 | from player_commands.weights import weights_cog 13 | from player_commands.leaderboard import leaderboard_cog 14 | from player_commands.price_check import price_check_cog 15 | from player_commands.minions import minions_cog 16 | from player_commands.rank import rank_cog 17 | from player_commands.guild_print import guild_print_cog 18 | from player_commands.maxer import maxer_cog 19 | from player_commands.notify import notify_cog 20 | from player_commands.duped import duped_cog 21 | 22 | from player_commands.set_prefix import set_prefix_cog 23 | from player_commands.link_account import link_account_cog 24 | from player_commands.help_command import help_cog 25 | from player_commands.regenerate_leaderboard import regenerate_leaderboard_cog 26 | from player_commands.populate_leaderboard import populate_leaderboard_cog 27 | 28 | from dev import dev_cog 29 | 30 | assistant_commands = [set_prefix_cog, link_account_cog, help_cog, 31 | regenerate_leaderboard_cog, populate_leaderboard_cog] 32 | 33 | 34 | regular_commands = [sky_cog, wiki_cog, bazaar_cog, 35 | dungeons_cog, kills_cog, lowest_bin_cog, 36 | skills_cog, slayer_cog, invite_cog, 37 | auction_house_cog, missing_cog, weights_cog, 38 | leaderboard_cog, price_check_cog, 39 | minions_cog, rank_cog, guild_print_cog, 40 | maxer_cog, notify_cog, duped_cog]+[dev_cog] 41 | 42 | player_commands = regular_commands+assistant_commands 43 | -------------------------------------------------------------------------------- /bot/player_commands/auction_house.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Iterator, Optional, Union 5 | 6 | import re 7 | import requests 8 | from datetime import datetime 9 | 10 | from utils import error, hf, format_duration, PROFILE_NAMES, bot_can_send, guild_ids 11 | from emojis import ITEM_RARITY 12 | from parse_profile import get_profile_data 13 | 14 | 15 | names = ["Expired/Ended auctions", "Buy It Now", "Auctions"] 16 | 17 | with open('text_files/hypixel_api_key.txt') as file: 18 | API_KEY = file.read() 19 | 20 | #=================================================================== 21 | def get_enchantments(lore: str) -> str: 22 | list_of_matches: Iterator = re.finditer(r"((?<=§9)|(?<=§d§l))([A-Za-z]+ )+(X|IX|VIII|VII|VI|V|IV|III|II|I)", lore) 23 | 24 | enchantment_list = [match.group(0) for match in list_of_matches] 25 | if not enchantment_list: return "" 26 | sorted_list = sorted(enchantment_list, key=lambda ench: ench.startswith("Ultimate"), reverse=True) 27 | 28 | enchantment_pairs: list = [sorted_list[i:i + 2] for i in range(0, len(sorted_list), 2)] 29 | if len(enchantment_pairs[-1]) == 1: 30 | enchantment_pairs[-1] = (enchantment_pairs[-1][0], "") 31 | 32 | enchantment_string: str = "\n".join([f"[{first.replace('_', ' ')}, {second.replace('_', ' ')}]".replace(", ]", "]") for first, second in enchantment_pairs]) 33 | 34 | formatted_enchants: str = f'''```ini 35 | [Enchantments] 36 | {enchantment_string} 37 | ``` 38 | ''' 39 | return formatted_enchants 40 | 41 | def to_time(time: int) -> datetime: 42 | return datetime.fromtimestamp(time/1000) 43 | 44 | #=================================================================== 45 | 46 | def format_auction(auction: dict) -> str: 47 | title = f"{ITEM_RARITY[auction['tier']]} {auction['item_name']}" 48 | expired = to_time(auction["end"]) < datetime.now() 49 | sell_type = "auction" if "bin" not in auction else "bin" 50 | 51 | bid_count = "" 52 | time_left = "" 53 | 54 | price = hf(auction.get("highest_bid_amount", 0) or auction.get("starting_bid", 0)) 55 | 56 | if expired and ('highest_bid_amount' in auction and auction['highest_bid_amount'] >= auction['starting_bid']): 57 | status = "SOLD" 58 | elif expired: 59 | status = "EXPIRED" 60 | elif sell_type == "auction": 61 | status = "BIDDING STAGE" 62 | else: 63 | status = "PURCHASABLE" 64 | 65 | if auction["claimed"] and len(auction["claimed_bidders"]) < len(set([x["bidder"] for x in auction["bids"]])): 66 | return "" 67 | 68 | if not expired: 69 | time_left = "↳ Time left: "+format_duration(to_time(auction["end"]) - datetime.now())+"\n" 70 | 71 | if sell_type == "auction": 72 | bid_count = f"↳ Bids: {len(auction.get('bids', []))}\n" 73 | price = f"{price} ({'Currently' if not expired else 'Final Price'})" 74 | else: # For BINS 75 | price = f"{price} {'(Buy it now)' if not expired else ''}" 76 | 77 | 78 | list_of_elems = [ 79 | f"**{title}**\n", 80 | f"↳ Status: {status}\n", 81 | f"{bid_count}", 82 | f"↳ Price: {price}\n", 83 | f"{time_left}",] 84 | 85 | return_string: str = "".join(list_of_elems)+get_enchantments(auction['item_lore']) 86 | return return_string 87 | 88 | #=================================================================== 89 | 90 | class auction_house_cog(commands.Cog): 91 | def __init__(self, bot) -> None: 92 | self.client = bot 93 | 94 | @commands.command(name="auction_house", aliases=['ah', 'auctions']) 95 | async def auction_house_command(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None) -> None: 96 | await self.get_auction_house(ctx, provided_username, provided_profile_name, is_response=False) 97 | 98 | @commands.slash_command(name="auctions", description="Gets auctions data about someone", guild_ids=guild_ids) 99 | async def auction_house_slash(self, ctx, username: Option(str, "username:", required=False), 100 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 101 | if not bot_can_send(ctx): 102 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 103 | await self.get_auction_house(ctx, username, profile, is_response=True) 104 | 105 | @commands.user_command(name="Get auction data", guild_ids=guild_ids) 106 | async def auction_house_context_menu(self, ctx, member: discord.Member): 107 | if not bot_can_send(ctx): 108 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 109 | await self.get_auction_house(ctx, member.display_name, None, is_response=True) 110 | 111 | #========================================================================================================================================= 112 | 113 | async def get_auction_house(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool = False) -> None: 114 | 115 | player_data: Optional[dict] = await get_profile_data(ctx, provided_username, provided_profile_name, is_response=is_response) 116 | if player_data is None: 117 | return 118 | username = player_data["username"] 119 | 120 | # From here, we have the right profile_id, so we can get all their auction data 121 | auction_data = requests.get(f"https://api.hypixel.net/skyblock/auction?key={API_KEY}&profile={player_data['profile_id']}").json() 122 | auctions: list[dict] = auction_data["auctions"] 123 | 124 | expired_auctions = [auction for auction in auctions if to_time(auction["end"]) < datetime.now()] 125 | bins = [auction for auction in auctions if to_time(auction["end"]) > datetime.now() and "bin" in auction and auction["bin"]] 126 | active_auctions = [auction for auction in auctions if to_time(auction["end"]) > datetime.now() and "bin" not in auction] 127 | 128 | data = [] 129 | 130 | for group_name, group in zip(names, [expired_auctions, bins, active_auctions]): 131 | if not group: 132 | continue 133 | data.append(f"**――――――― {group_name} ―――――――**") 134 | for auction in group: 135 | auction_string = format_auction(auction) 136 | if auction_string == "": 137 | continue 138 | if sum([len(x) for x in data])+len(auction_string) > 4000: 139 | break 140 | data.append(auction_string) 141 | if data[-1] == f"**――――――― {group_name} ―――――――**": 142 | data = data[:-1] 143 | 144 | if not data: 145 | return await error(ctx, f"{username} doesn't have any active auctions", "If this was you, try heading over to the auction house and putting some things on sale.", is_response=is_response) 146 | #===================== 147 | embed = discord.Embed(title=f"Auction data for {username}", description="\n".join(data) , colour=0x3498DB) 148 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 149 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 150 | if is_response: 151 | await ctx.respond(embed=embed) 152 | else: 153 | await ctx.send(embed=embed) 154 | -------------------------------------------------------------------------------- /bot/player_commands/bazaar.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import json 7 | import requests 8 | from difflib import SequenceMatcher 9 | 10 | from utils import error, similar, bot_can_send, guild_ids 11 | 12 | initial_request = requests.get(f"https://api.hypixel.net/skyblock/bazaar").json() 13 | ITEMS = initial_request["products"].keys() 14 | 15 | with open("text_files/MASTER_ITEM_DICT.json", "r", encoding="utf-8") as file: 16 | ITEM_DICT: dict = json.load(file) 17 | 18 | items_mapped: list[tuple] = [] 19 | for item in ITEMS: 20 | try: 21 | data = (item, ITEM_DICT[item]["name"]) 22 | items_mapped.append(data) 23 | except KeyError: 24 | pass 25 | 26 | ## The reason I don't use find-closest is because I only want to auto-correct to bazaar items, not all items 27 | 28 | class bazaar_cog(commands.Cog): 29 | def __init__(self, bot) -> None: 30 | self.client = bot 31 | 32 | @commands.command(name="bazaar", aliases=['b', 'ba', 'baz', 'bz']) 33 | async def bazaar_command(self, ctx, *, input: Optional[str] = None) -> None: 34 | await self.bazaar(ctx, input, is_response=False) 35 | 36 | @commands.slash_command(name="bazaar", description="Gets bazaar data for a certain item", guild_ids=guild_ids) 37 | async def bazaar_slash(self, ctx, input: Option(str, "input:", required=True)): 38 | if not bot_can_send(ctx): 39 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 40 | await self.bazaar(ctx, input, is_response=True) 41 | 42 | #========================================================================================================================= 43 | 44 | async def bazaar(self, ctx, user_input: Optional[str] = None, is_response: bool = False) -> None: 45 | if user_input is None: 46 | return await error(ctx, "No item given.", "Please give the item you want to check the price of at the bazaar.", is_response=is_response) 47 | 48 | if user_input.startswith("e "): 49 | user_input = "enchanted"+user_input.removeprefix("e ") 50 | 51 | closest = max(items_mapped, key=lambda _tuple: similar(_tuple[1].lower(), user_input.lower())) 52 | 53 | if similar(closest[1], user_input) < 0.5: 54 | return await error(ctx, "No item with that name found at the bazaar.", "Is the item availible to purchase at the bazaar?", is_response=is_response) 55 | 56 | request = requests.get(f"https://api.hypixel.net/skyblock/bazaar").json() 57 | 58 | internal_name = closest[0] 59 | 60 | data = request["products"][internal_name] 61 | 62 | list_of_strings = [ 63 | f"Buy it instantly price: ${int(data['buy_summary'][0]['pricePerUnit'])}", 64 | f"Sell it instantly price: ${int(data['sell_summary'][0]['pricePerUnit'])}", 65 | f"Buy volume: {data['quick_status']['buyVolume']}", 66 | f"Sell volume: {data['quick_status']['sellVolume']}", 67 | ] 68 | 69 | embed = discord.Embed(title=f"Bazaar pricing information for {closest[1]}", description="\n".join(list_of_strings), url=f"https://bazaartracker.com/product/{internal_name.lower()}", colour=0x3498DB) 70 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 71 | if is_response: 72 | await ctx.respond(embed=embed) 73 | else: 74 | await ctx.send(embed=embed) 75 | -------------------------------------------------------------------------------- /bot/player_commands/duped.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | 8 | from utils import error, PROFILE_NAMES, remove_colours, bot_can_send, guild_ids 9 | from parse_profile import get_profile_data 10 | from extract_ids import extract_nbt_dicts, parse_container 11 | 12 | class duped_cog(commands.Cog): 13 | def __init__(self, bot) -> None: 14 | self.client = bot 15 | 16 | @commands.command(name="duped", aliases=['duped_profile', 'dupe']) 17 | async def duped_command(self, ctx, provided_username: Optional[str] = None, provided_profile: Optional[str] = None) -> None: 18 | await self.get_duped(ctx, provided_username, provided_profile, is_response=False) 19 | 20 | @commands.slash_command(name="duped", description="See a list of all player's items that are most likely to be duped.", guild_ids=guild_ids) 21 | async def duped_slash(self, ctx, username: Option(str, "username:", required=False), 22 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 23 | if not bot_can_send(ctx): 24 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 25 | await ctx.defer() 26 | await self.get_duped(ctx, username, profile, is_response=True) 27 | 28 | #========================================================================================================================================= 29 | 30 | async def get_duped(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool = False) -> None: 31 | 32 | player_data: Optional[dict] = await get_profile_data(ctx, provided_username, provided_profile_name, is_response=is_response) 33 | if player_data is None: 34 | return 35 | username = player_data["username"] 36 | 37 | #================================================================================== 38 | inv_content = player_data.get("inv_contents", {"data": ""}) 39 | inventory = extract_nbt_dicts(inv_content["data"]) 40 | #''' 41 | ender_chest = player_data.get("ender_chest_contents", {"data": ""}) 42 | ender_chest = extract_nbt_dicts(ender_chest["data"]) 43 | 44 | storage_items = [] 45 | if player_data.get("backpack_contents"): 46 | for i in range(0, 19): 47 | page = player_data["backpack_contents"].get(str(i), {"data": ""}) 48 | try: 49 | page_data = extract_nbt_dicts(page["data"]) 50 | storage_items.extend(page_data) 51 | except TypeError: 52 | pass 53 | #''' 54 | list_of_items = inventory+ender_chest+storage_items 55 | filtered_list_of_items = [x for x in list_of_items if x.get("ExtraAttributes", {}).get("uuid", False)] 56 | list_of_uuids = [x["ExtraAttributes"]["uuid"] for x in filtered_list_of_items] 57 | #################################################################################### 58 | 59 | #pets = [x.get("uuid") for x in player_data.get("pets", []) if x.get("uuid")] 60 | pets = [] 61 | 62 | try: 63 | duped_uuids = requests.post("https://sky.coflnet.com/api/auctions/active/uuid", json=list_of_uuids+pets).json() 64 | except: 65 | return await error(ctx, f"Error, Duped API is down!", f"Please wait for it to return, and try again later!", is_response=is_response) 66 | 67 | if not duped_uuids: 68 | embed = discord.Embed(title=f"The player you inputted likely had no duped items.", description="This command works the majority of the time, however isn't 100% accurate, sometimes it can't find duped items.", colour=0x3498DB) 69 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 70 | if is_response: 71 | return await ctx.respond(embed=embed) 72 | else: 73 | return await ctx.send(embed=embed) 74 | 75 | #print(duped_uuids) 76 | list_of_duped_items = [] 77 | for uuid in duped_uuids: 78 | for item in filtered_list_of_items: 79 | if not item.get("ExtraAttributes"): 80 | if item["uuid"] == uuid: 81 | list_of_duped_items.append(item) 82 | else: 83 | if item["ExtraAttributes"]["uuid"] == uuid: 84 | list_of_duped_items.append(item) 85 | 86 | desc = "\n".join(remove_colours(x["display"]["Name"]) for x in list_of_duped_items) 87 | 88 | embed = discord.Embed(title=f"{username}'s list of duped items:", url=f"https://sky.shiiyu.moe/stats/{username}", description=desc, colour=0x3498DB) 89 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 90 | embed.set_thumbnail(url=f"https://cravatar.eu/helmhead/{username}") 91 | if is_response: 92 | await ctx.respond(embed=embed) 93 | else: 94 | await ctx.send(embed=embed) 95 | 96 | -------------------------------------------------------------------------------- /bot/player_commands/guild_print.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | 4 | import requests 5 | from bisect import bisect 6 | 7 | from utils import error, hf, API_KEY 8 | from emojis import SKILL_EMOJIS 9 | from parse_profile import get_profile_data 10 | 11 | 12 | class guild_print_cog(commands.Cog): 13 | def __init__(self, bot) -> None: 14 | self.client = bot 15 | 16 | @commands.command(name="guild_print") 17 | async def guild_print_command(self, ctx, skill: str=None, guild: str="5d2e5aa477ce8415c3fd00e8") -> None: 18 | 19 | print("Starting a guild print", skill) 20 | if skill is None or skill.lower() not in ['farming', 'mining', 'combat', 'foraging', 'fishing', 'enchanting', 'alchemy', 'taming', 'carpentry', 'runecrafting', 'catacombs']: 21 | return await error(ctx, "Error, invalid skill!", "This command takes a skill and a guild id to work") 22 | 23 | try: 24 | data = requests.get(f"https://api.hypixel.net/guild?key={API_KEY}&id={guild}").json()["guild"] 25 | members_uuid = [x['uuid'] for x in data['members']] 26 | guild_name = data['name'] 27 | except: 28 | return await error(ctx, "Error, something messed up but I'm not sure what, I'll fix it when I can.") 29 | 30 | #print(f"Guild name = {guild_name}") 31 | list_of_strings = [] 32 | for uuid in members_uuid: 33 | # Get the username 34 | username = requests.get(f"https://api.mojang.com/user/profiles/{uuid}/names").json()[-1]["name"] 35 | print(f"Username: {username}, uuid: {uuid}") 36 | profile_list = requests.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}").json() 37 | if not profile_list["profiles"]: 38 | continue 39 | valid_profiles: list[dict] = [x for x in profile_list["profiles"] if "last_save" in x['members'][uuid]] 40 | profile_data = max(valid_profiles, key=lambda x: x['members'][uuid]['last_save']) 41 | profile = profile_data["members"][uuid] 42 | # Extract the right skill 43 | if skill != "catacombs": 44 | skill_value = profile.get(f"experience_skill_{skill}", "HIDDEN") 45 | else: 46 | skill_value = profile["dungeons"]["dungeon_types"]["catacombs"].get('experience', "HIDDEN") 47 | nice_num = f"{int(skill_value):,}" if skill_value != "HIDDEN" else skill_value 48 | list_of_strings.append(f"**{username}**: {nice_num}") 49 | 50 | embed = discord.Embed(title=f"{guild_name}'s current {skill} stats:", description="\n".join(list_of_strings), colour=0x3498DB) 51 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 52 | await ctx.send(embed=embed) 53 | -------------------------------------------------------------------------------- /bot/player_commands/help_command.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | 4 | from utils import bot_can_send, guild_ids 5 | from menus import generate_static_preset_menu 6 | 7 | data_dict = { 8 | "networth": ("[username]", "Checks the total value of a profile for a user."), 9 | "auctions": ("[username]", "Shows someone's auctions and BINs."), 10 | "bazaar": ("", "Shows the bazaar price for a certain item, e.g. 'cobblestone'."), 11 | "dungeons": ("[username]", "Shows data about someone's dungeon level, including what floors they've beaten."), 12 | "duped": ("[username]", "Shows duped items that a player has on them."), 13 | "help": ("", "Takes you to this command."), 14 | "invite": ("", "Provides instructions on how to invite the bot to your server."), 15 | "kills": ("[username]", "Shows the most mobs a player has killed."), 16 | "leaderboard": ("[profile_type]","Shows the top 100 players with the highest combined networth!"), 17 | "link": ("[ign]", "Links your in-game name to your discord id so you can leave it out in commands."), 18 | "lowest_bin": ("", "Shows the lowest bin on auction house with that name, use .lb for short!"), 19 | "maxer": ("[username]", "Shows all the attributes and enchantments of the item you select."), 20 | "minions": ("[username]", "Shows the cheapest minions to craft to increase your unique crafted minions count."), 21 | "missing": ("[username]", "Shows the top tier accessories that the player is missing."), 22 | "price_check": ("", "Shows historic pricing data about the given item, use .p for short!"), 23 | "set_prefix": ("", "Allows an admin to change the prefix of the bot."), 24 | "skills": ("[username]", "Shows a summary of all a users skills, including average."), 25 | "sky": ("[username]", "Links you to someone's sky.shiiyu.moe page, for convenience."), 26 | "slayer": ("[username]", "Shows a summary of someone's slayer data, including how many of each tier someone's killed."), 27 | "rank": ("[username]", "Shows where you place amongst other players with the value of your profile."), 28 | "weight": ("[username]", "Shows a menu of weights which represent how far into the game the player is."), 29 | "wiki": ("", "Shows a page from the Hypixel wiki, to assist in finding a page or item."), 30 | "weights": ("[username]", "Shows someone's senither weight (and overflow weight) in different sections."), 31 | } 32 | 33 | categories = { 34 | "Player Stats Commands": ["dungeons", "kills", "missing", "rank", "skills", "sky", "slayer", "weights"], 35 | "Price Data Commands": ["auctions", "bazaar", "lowest_bin", "networth", "price_check"], 36 | "General Info Commands": ["duped", "leaderboard", "maxer", "minions", "rank", "wiki"], 37 | "Settings Commands": ["help", "invite", "link", "set_prefix"], 38 | } 39 | 40 | EMOJI_LIST = ["<:stats:915209828983537704>", "<:general_info:915210726900125706>", "<:price_data:915211058728275980>", "<:settings:915211248684134420>"] 41 | 42 | class help_cog(commands.Cog): 43 | def __init__(self, bot): 44 | self.client = bot 45 | 46 | @commands.command(name="help", aliases=["h", "he", "hel"]) 47 | async def help_command(self, ctx) -> None: 48 | await self.help(ctx, is_response=False) 49 | 50 | @commands.slash_command(name="help", description="Shows the help command", guild_ids=guild_ids) 51 | async def help_slash(self, ctx): 52 | if not bot_can_send(ctx): 53 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 54 | await self.help(ctx, is_response=True) 55 | 56 | #============================================================================================================================ 57 | 58 | async def help(self, ctx, is_response: bool = False) -> None: 59 | if ctx.guild.id == 1036024217080168488: 60 | return 61 | list_of_embeds = [] 62 | for category, commands in categories.items(): 63 | embed = discord.Embed(title=category, colour=0x3498DB) 64 | for command in commands: 65 | param, description = data_dict[command] 66 | param = "" if param == "" else f" {param}" 67 | embed.add_field(name=f"{command}{param}", value=description, inline=False) 68 | 69 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 70 | list_of_embeds.append(embed) 71 | 72 | await generate_static_preset_menu(ctx=ctx, list_of_embeds=list_of_embeds, emoji_list=EMOJI_LIST, is_response=is_response) 73 | -------------------------------------------------------------------------------- /bot/player_commands/invite.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import permissions # type: ignore 4 | 5 | from utils import bot_can_send, guild_ids 6 | 7 | #''' 8 | def custom_check(ctx): 9 | print("This will be printed on startup") 10 | print(a) 11 | return (ctx.channel.permissions_for(ctx.guild.me)).send_messages 12 | async def predicate(ctx): 13 | print("This will be printed when the predicate is called") 14 | print((ctx.channel.permissions_for(ctx.guild.me)).send_messages) 15 | return (ctx.channel.permissions_for(ctx.guild.me)).send_messages 16 | 17 | return commands.check(predicate) 18 | #''' 19 | 20 | ''' 21 | def custom_check(): 22 | print("Check!") 23 | print((ctx.channel.permissions_for(ctx.guild.me)).send_messages) 24 | return (ctx.channel.permissions_for(ctx.guild.me)).send_messages 25 | #''' 26 | 27 | ''' 28 | This is what I used for commands 29 | def allowed_channels(allowed_channels_list): 30 | async def predicate(ctx): 31 | return ctx.guild and (ctx.channel.id in allowed_channels_list) 32 | return commands.check(predicate) 33 | 34 | @allowed_channels([PREFIX_COMMAND]) 35 | ''' 36 | 37 | class invite_cog(commands.Cog): 38 | def __init__(self, bot) -> None: 39 | self.client = bot 40 | 41 | @commands.command(name="invite") 42 | async def invite_command(self, ctx) -> None: 43 | await self.invite(ctx, is_response=False) 44 | 45 | #@custom_check() 46 | #@commands.has_permissions(send_messages=True) 47 | @commands.slash_command(name="invite", description="Shows info on inviting the bot", guild_ids=guild_ids)#, checks=[custom_check, ]) 48 | async def invite_slash(self, ctx): 49 | #if not bot_can_send(ctx): 50 | # return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 51 | await self.invite(ctx, is_response=True) 52 | 53 | #========================================================================================================================================= 54 | 55 | async def invite(self, ctx, is_response: bool = False) -> None: 56 | invite_link = "https://discord.com/api/oauth2/authorize?client_id=854722092037701643&permissions=67488768&scope=bot%20applications.commands" 57 | topgg_link = "https://top.gg/bot/854722092037701643" 58 | embed = discord.Embed(title=f"Want to invite this bot to your server?", description=f"You can directly add this bot to your server by going on it's profile and clicking 'Add to Server'. Alternatively, go to [this link]({invite_link}) to invite the bot manually, or [this link]({topgg_link}) to see the top.gg page and enjoy all the awesome features. Default prefix is `.` (or slash commands) but can be changed with `.set_prefix`.", colour=0x3498DB) 59 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 60 | if is_response: 61 | await ctx.respond(embed=embed) 62 | else: 63 | await ctx.send(embed=embed) 64 | -------------------------------------------------------------------------------- /bot/player_commands/kills.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | from bisect import bisect 8 | 9 | from parse_profile import get_profile_data 10 | from utils import error, format_duration, clean, PROFILE_NAMES, bot_can_send, guild_ids 11 | 12 | def comma_seperate(num: float) -> str: 13 | return f"{int(num):,}" # var:, = 10,000 (the comma) 14 | 15 | class kills_cog(commands.Cog): 16 | def __init__(self, bot) -> None: 17 | self.client = bot 18 | 19 | @commands.command(name="kills", aliases=['k', 'kill']) 20 | async def kills_command(self, ctx, provided_username: Optional[str] = None, provided_profile: Optional[str] = None) -> None: 21 | await self.get_kills(ctx, provided_username, provided_profile, is_response=False) 22 | 23 | @commands.slash_command(name="kills", description="Gets the entities the player has killed the most", guild_ids=guild_ids) 24 | async def kills_slash(self, ctx, username: Option(str, "username:", required=False), 25 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 26 | if not bot_can_send(ctx): 27 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 28 | await self.get_kills(ctx, username, profile, is_response=True) 29 | 30 | #========================================================================================================================================= 31 | 32 | async def get_kills(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool = False) -> None: 33 | 34 | player_data: Optional[dict] = await get_profile_data(ctx, provided_username, provided_profile_name, is_response=is_response) 35 | if player_data is None: 36 | return 37 | username = player_data["username"] 38 | 39 | stats = player_data["stats"] 40 | total_mobs_killed = f"**{comma_seperate(stats['kills'])}**" if "kills" in stats else "Unknown" 41 | 42 | kills_stats = {k: v for k, v in stats.items() if k.startswith("kills_")} 43 | sorted_kills = dict(sorted(kills_stats.items(), key=lambda mob: mob[1], reverse=True)[:12]) 44 | 45 | embed = discord.Embed(title=f"{username}", url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 46 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 47 | 48 | embed.add_field(name=f"Kills Data", value=f"Total Mobs Killed {total_mobs_killed}", inline=False) 49 | 50 | for index, (key, value) in enumerate(sorted_kills.items(), 1): 51 | formatted_name = key.removeprefix("kills_").replace('_', ' ').title().replace('Unburried Zombie', 'Crypt Ghoul') 52 | embed.add_field(name=f"#{index} {formatted_name}", value=f":knife: {comma_seperate(value)}", inline=True) 53 | 54 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 55 | if is_response: 56 | await ctx.respond(embed=embed) 57 | else: 58 | await ctx.send(embed=embed) 59 | -------------------------------------------------------------------------------- /bot/player_commands/leaderboard.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests # For fetching the player's head 7 | import json # For dumping the uuid cache 8 | 9 | from database_manager import get_max_current_networth 10 | from utils import hf, error, bot_can_send, guild_ids 11 | from menus import generate_static_scrolling_menu 12 | 13 | MISSING_EMOJI = "<:player_head:876942582444343347>" 14 | 15 | async def emoji_page(client: commands.Bot, page: int, username: str, use_emojis: bool=True) -> str: 16 | # If it's the top 3 emojis, and we're using emojis (i.e. not iron man) 17 | if page not in [1, 2, 3] or not use_emojis: 18 | return MISSING_EMOJI 19 | 20 | emoji = discord.utils.find(lambda emoji: emoji.name.lower() == username.lower(), client.emoji_guild.emojis) 21 | if emoji is not None: # If the emoji already exists. 22 | return emoji 23 | 24 | print(f"Couldn't find emoji with username: {username}") 25 | #image_request = requests.get(f"https://mc-heads.net/head/{username}") 26 | #if image_request.status_code != 200: # 503: This will sometimes return 503 for some reason, 503 Service Unavailable 27 | # return MISSING_EMOJI 28 | 29 | print("#"*40+f"Creating new emoji for {username}") 30 | try: 31 | #new_emoji = await client.emoji_guild.create_custom_emoji(name=username, image=image_request.content) 32 | new_emoji = None 33 | except Exception as e: 34 | print(f"Creating a new emoji for {username} failed", e) 35 | return new_emoji or MISSING_EMOJI # Return the emoji if it's not None 36 | 37 | ################################# 38 | async def page_generator(ctx, data: list, page: int, use_emojis: bool) -> discord.Embed: 39 | embed = discord.Embed(colour=0x3498DB) 40 | cropped_data = data[(page-1)*10:page*10] 41 | client = ctx.bot 42 | 43 | for i, (uuid, total) in enumerate(cropped_data, 1): 44 | if uuid not in client.uuid_conversion_cache: 45 | client.uuid_conversion_cache[uuid] = requests.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}").json()["name"] 46 | 47 | username = client.uuid_conversion_cache[uuid] 48 | emoji_text = await emoji_page(client, page, username, use_emojis) 49 | 50 | embed.add_field(name=f"{emoji_text} {username} - #{i+(page-1)*10}", value=f"Total: **{hf(total)}**", inline=True) 51 | 52 | with open("text_files/uuid_conversion_cache.json", 'w') as file: 53 | json.dump(client.uuid_conversion_cache, file) 54 | 55 | embed.set_author(icon_url="https://media.discordapp.net/attachments/854829960974565396/868236867944972368/crown.png", name=f"Networth Leaderboard (current), page {page}:") 56 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 57 | 58 | return embed 59 | 60 | class leaderboard_cog(commands.Cog): 61 | def __init__(self, bot) -> None: 62 | self.client = bot 63 | 64 | @commands.command(name="leaderboard", aliases=["top", "l", "nwlb"]) 65 | async def leaderboard_command(self, ctx, provided_profile_type: Optional[str] = "regular") -> None: 66 | await self.leaderboard(ctx, provided_profile_type, False) 67 | 68 | @commands.slash_command(name="leaderboard", description="Gets the top Skyblock players", guild_ids=guild_ids) 69 | async def leaderboard_slash(self, ctx, profile_type: Option(str, "profile_type", choices=['regular', 'ironman'], required=False, default="regular")): 70 | if not bot_can_send(ctx): 71 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 72 | await ctx.defer() 73 | await self.leaderboard(ctx, profile_type, is_response=True) 74 | 75 | #=================================================================================================================================== 76 | 77 | async def leaderboard(self, ctx, provided_profile_type: Optional[str] = "regular", is_response: bool = False) -> None: 78 | 79 | profile_type = provided_profile_type.lower() 80 | if profile_type not in ['regular', 'ironman']: 81 | return await error(ctx, "Error, invalid profile type", "Valid profile types include 'regular' or 'ironman'", is_response=is_response) 82 | 83 | data = get_max_current_networth(profile_type) 84 | list_of_embeds = [] 85 | 86 | for page in range(1, 11): 87 | embed = await page_generator(ctx, data, page, use_emojis=(profile_type=="regular")) 88 | list_of_embeds.append(embed) 89 | 90 | await generate_static_scrolling_menu(ctx=ctx, list_of_embeds=list_of_embeds, is_response=is_response) 91 | -------------------------------------------------------------------------------- /bot/player_commands/link_account.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | from database_manager import * 7 | from utils import error, bot_can_send, guild_ids 8 | 9 | class link_account_cog(commands.Cog): 10 | def __init__(self, bot) -> None: 11 | self.client = bot 12 | 13 | @commands.command(name="link_account", aliases=["linkaccount", "link"]) 14 | async def link_account_command(self, ctx, username: Optional[str] = None) -> None: 15 | await self.link_account(ctx, username, is_response=False) 16 | 17 | @commands.slash_command(name="link_account", description="Links your discord account to a minecraft account", guild_ids=guild_ids) 18 | async def link_account_slash(self, ctx, username: Option(str, "username:", required=True)): 19 | if not bot_can_send(ctx): 20 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 21 | await self.link_account(ctx, username, is_response=True) 22 | 23 | #================================================================================================================== 24 | 25 | async def link_account(self, ctx, username: Optional[str] = None, is_response: bool = False) -> None: 26 | if username is None: 27 | return await error(ctx, "Command link_account must have a username!", f"Example usage: `{ctx.prefix}link_account Notch`", is_response=is_response) 28 | if not (3 < len(username) <= 16): 29 | return await error(ctx, "Error, invalid username set!", "The username given must be a valid minecraft account!", is_response=is_response) 30 | 31 | current_linked_account: Optional[str] = self.client.linked_accounts.get(f"{ctx.author.id}", None) 32 | 33 | if current_linked_account is None: 34 | set_linked_account(ctx.author.id, username) 35 | else: 36 | update_linked_account(ctx.author.id, username) 37 | 38 | self.client.linked_accounts[f"{ctx.author.id}"] = username 39 | 40 | embed = discord.Embed(title=f"Your linked account for Community Bot has been updated.", description=f"{ctx.author.display_name} has updated their linked account for community bot, it's now `{username}`", colour=0xe67e22) 41 | embed.set_footer(text=f"Use {'/' if is_response else ctx.prefix}link_account to change the account linked to Community Bot") 42 | if is_response: 43 | await ctx.respond(embed=embed) 44 | else: 45 | await ctx.send(embed=embed) 46 | -------------------------------------------------------------------------------- /bot/player_commands/lowest_bin.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | from datetime import datetime # To convert hypixel time string to object 8 | 9 | from utils import error, hf, format_duration, smarter_find_closest, bot_can_send, guild_ids 10 | from emojis import ITEM_RARITY 11 | from menus import generate_static_scrolling_menu 12 | 13 | 14 | def format_enchantments(enchantments: list[str]) -> str: 15 | if not enchantments: 16 | return "" 17 | sorted_enchants = sorted(enchantments, key=lambda ench: ench.startswith("Ultimate"), reverse=True) 18 | 19 | enchantment_pairs = [sorted_enchants[i:i + 2] for i in range(0, len(sorted_enchants), 2)] 20 | if len(enchantment_pairs[-1]) == 1: 21 | enchantment_pairs[-1] = (enchantment_pairs[-1][0], "") 22 | 23 | enchantment_string = "\n".join([f"[{first.replace('_', ' ')}, {second.replace('_', ' ')}]".replace(", ]", "]") for first, second in enchantment_pairs]) 24 | 25 | formatted_enchants = f'''```ini 26 | [Enchantments] 27 | {enchantment_string} 28 | ``` 29 | '''.rstrip("\n") 30 | return formatted_enchants 31 | 32 | class lowest_bin_cog(commands.Cog): 33 | def __init__(self, bot) -> None: 34 | self.client = bot 35 | 36 | @commands.command(name="lowest_bin", aliases=['lb', 'bin', 'lbin']) 37 | async def lowest_bin_command(self, ctx, *, input: Optional[str] = None) -> None: 38 | await self.lowest_bin(ctx, input, is_response=False) 39 | 40 | @commands.slash_command(name="lowest_bin", description="Gets the top 10 lowest BINs of that item on the AH", guild_ids=guild_ids) 41 | async def lowest_bin_slash(self, ctx, input: Option(str, "input:", required=True)): 42 | if not bot_can_send(ctx): 43 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 44 | await self.lowest_bin(ctx, input, is_response=True) 45 | 46 | #=============================================================================================================== 47 | 48 | async def lowest_bin(self, ctx, input: Optional[str] = None, is_response: bool = False) -> None: 49 | closest = await smarter_find_closest(ctx, input) 50 | if closest is None: 51 | return 52 | 53 | if closest[0] == "enchant": 54 | enchant_type, level = closest[1].split(":") 55 | response = requests.get(f"https://sky.coflnet.com/api/auctions/tag/ENCHANTED_BOOK/active/bin?Enchantment={enchant_type}&EnchantLvl={level}") 56 | elif closest[0] == "pet": 57 | pet_type, rarity, level = closest[1].split(":") 58 | rarity_selector = "" if rarity == "None" else f"Rarity={rarity.upper()}" 59 | level_selector = "" if level == "None" else f"PetLevel={level}" 60 | if level != "None" and rarity != "None": 61 | level_selector = "&"+level_selector 62 | response = requests.get(f"https://sky.coflnet.com/api/auctions/tag/PET_{pet_type.upper()}/active/bin?{rarity_selector}{level_selector}") 63 | 64 | elif closest[0] == "item": 65 | closest = closest[1] 66 | response = requests.get(f"https://sky.coflnet.com/api/auctions/tag/{closest['internal_name']}/active/bin") 67 | 68 | try: 69 | response = response.json() 70 | except Exception as e: 71 | print(response.status_code) 72 | print(e) 73 | print(response.text) 74 | 75 | if not response or (isinstance(response, dict) and "Slug" in response.keys()): 76 | return await error(ctx, "Error, no items of that type could be found on the auction house!", "If you're searching for a pet, please end your search with 'pet', and if you're searching for an ultimate enchantment, please include `ultimate`.", is_response=is_response) 77 | 78 | list_of_embeds = [] 79 | for page, data in enumerate(response, 1): 80 | # -> # 2021-07-30T11:06:19Z 81 | time_left: str = format_duration(datetime.strptime(data['end'].rstrip("Z"), '%Y-%m-%dT%H:%M:%S')) 82 | 83 | # Enchants 84 | #print(data["enchantments"]) # int object has no attribute title 85 | enchantment_list: list[str] = [x["type"].title()+f" {x['level']}" for x in data["enchantments"]] 86 | enchantments: str = format_enchantments(enchantment_list) 87 | # Hot potato books 88 | hot_potato_books: str = data["flatNbt"].get("hpc", "") 89 | if hot_potato_books: 90 | if int(hot_potato_books) > 10: 91 | hot_potato_books = f"\n\nThis item has 10 hot potato books, and {int(hot_potato_books)-10} fuming potato books" 92 | else: 93 | hot_potato_books = f"\n\nThis item has {hot_potato_books} hot potato books" 94 | 95 | # Format the auction 96 | formatted_auction = f"↳ Price: {hf(data['startingBid'])}\n↳ Time Remaining: {time_left}"+hot_potato_books+("\n" if not enchantments else enchantments)+f"\nTo view this auction ingame, type this command in chat:\n `/ah {data['auctioneerId']}`" 97 | 98 | embed = discord.Embed(title=f"Lowest bin found for {ITEM_RARITY[data['tier']]} {data['itemName']}, Page {page}:", description=formatted_auction, colour=0x3498DB) 99 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 100 | 101 | list_of_embeds.append(embed) 102 | 103 | await generate_static_scrolling_menu(ctx=ctx, list_of_embeds=list_of_embeds, is_response=is_response) 104 | 105 | -------------------------------------------------------------------------------- /bot/player_commands/missing.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import json 7 | import requests 8 | 9 | from utils import error, PROFILE_NAMES, hf, bot_can_send, guild_ids 10 | from menus import generate_static_preset_menu 11 | from emojis import ITEM_RARITY 12 | from parse_profile import get_profile_data 13 | from extract_ids import extract_internal_names 14 | 15 | RARITY_LIST = list(ITEM_RARITY.keys()) 16 | 17 | # Create the master list! 18 | from text_files.accessory_list import talisman_upgrades 19 | 20 | # Get a list of all accessories 21 | ACCESSORIES: list[dict] = [] 22 | with open("text_files/MASTER_ITEM_DICT.json", "r", encoding="utf-8") as file: 23 | item_dict = json.load(file) 24 | for item in item_dict: 25 | if item_dict[item].get("rarity", False) and item_dict[item]["rarity"] != "UNKNOWN": 26 | ACCESSORIES.append(item_dict[item]) 27 | 28 | # Now remove all the low tier ones 29 | MASTER_ACCESSORIES = [] 30 | for accessory in ACCESSORIES: 31 | if accessory["internal_name"] not in talisman_upgrades.keys(): 32 | MASTER_ACCESSORIES.append(accessory) 33 | 34 | EMOJI_LIST = ["<:alphabetically:905066318720544779>", "<:recombobulator:854750106376339477>", "<:by_price:900069290143797299>", "<:magic_power:1052617112193093723>"] 35 | NO_PRICE = 9876543211 36 | 37 | MAGIC_POWER_BY_RARITY = { 38 | "COMMON": 3, 39 | "UNCOMMON": 5, 40 | "RARE": 8, 41 | "EPIC": 12, 42 | "LEGENDARY": 16, 43 | "MYTHIC": 22, 44 | "SPECIAL": 3, 45 | "VERY_SPECIAL": 5, 46 | } 47 | class missing_cog(commands.Cog): 48 | def __init__(self, bot) -> None: 49 | self.client = bot 50 | 51 | @commands.command(name="missing", aliases=['missing_accessories', 'accessories', 'miss', 'm']) 52 | async def missing_command(self, ctx, provided_username: Optional[str] = None, provided_profile: Optional[str] = None) -> None: 53 | await self.get_missing(ctx, provided_username, provided_profile, is_response=False) 54 | 55 | @commands.slash_command(name="missing", description="Gets someone's missing accessories", guild_ids=guild_ids) 56 | async def missing_slash(self, ctx, username: Option(str, "username:", required=False), 57 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 58 | if not bot_can_send(ctx): 59 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 60 | await ctx.defer() 61 | await self.get_missing(ctx, username, profile, is_response=True) 62 | 63 | #========================================================================================================================================= 64 | 65 | async def get_missing(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool = False) -> None: 66 | 67 | player_data: Optional[dict] = await get_profile_data(ctx, provided_username, provided_profile_name, is_response=is_response) 68 | if player_data is None: 69 | return 70 | username = player_data["username"] 71 | 72 | accessory_bag = player_data.get("talisman_bag", None) 73 | inv_content = player_data.get("inv_contents", {"data": []}) 74 | 75 | if not accessory_bag: 76 | return await error(ctx, "Error, could not find this person's accessory bag", "Do they have their API disabled for this command?", is_response=is_response) 77 | 78 | accessory_bag = extract_internal_names(accessory_bag["data"]) 79 | inventory = extract_internal_names(inv_content["data"]) 80 | 81 | missing = [x for x in MASTER_ACCESSORIES if x["internal_name"] not in accessory_bag+inventory] 82 | 83 | if not missing: 84 | return await error(ctx, f"Completion!", f"{username} already has all accessories!", is_response=is_response) 85 | 86 | try: 87 | lowest_bin_data = requests.get("http://moulberry.codes/lowestbin.json").json() 88 | except: 89 | return await error(ctx, f"Error, price API is down!", f"Please wait for it to return, and try again later!", is_response=is_response) 90 | 91 | for accessory in missing: 92 | accessory["price"] = lowest_bin_data.get(accessory["internal_name"], NO_PRICE) 93 | 94 | list_of_embeds = [] 95 | 96 | for page, parameter, sort_func in zip( 97 | ["alphabetically", "by rarity", "by price", "by magic power"], ["name", "rarity", "price", "magic power"], 98 | [lambda x: x["name"], lambda x: RARITY_LIST.index(x["rarity"]), lambda x: x["price"], lambda x: x["price"]/MAGIC_POWER_BY_RARITY[x["rarity"]]] 99 | ): 100 | sorted_accessories = sorted(missing, key=sort_func)[:42] 101 | 102 | extra = "" if len(missing) <= 36 else f", showing the first {int(len(sorted_accessories)/6)}" 103 | embed = discord.Embed(title=f"Missing {len(missing)} accessories for {username}{extra}, sorted: {page}", colour=0x3498DB) 104 | 105 | def make_embed(embed, acc_list, num): 106 | text = "" 107 | for item in acc_list: 108 | wiki_link = "" if not item['wiki_link'] else f"[wiki]({item['wiki_link']})" 109 | if parameter == "magic power": 110 | coins_per_mp = int(item["price"]/MAGIC_POWER_BY_RARITY[item["rarity"]]) 111 | price = f"{hf(coins_per_mp)} coins/mp" if item['price'] != NO_PRICE else 'N/A' 112 | else: 113 | price = hf(item['price']) if item['price'] != NO_PRICE else 'N/A' 114 | text += f"{ITEM_RARITY[item['rarity']]} {item['name']}\n➜ For {price}, link: {wiki_link}\n" 115 | 116 | embed_title = f"{acc_list[0]['name'][0]}-{acc_list[-1]['name'][0]}" if parameter == "name" else f"Group {num}" 117 | embed.add_field(name=f"{embed_title}", value=text, inline=True) 118 | 119 | if len(sorted_accessories) < 6: # For people with only a few missing 120 | make_embed(embed, sorted_accessories, 1) 121 | else: 122 | list_length = int(len(sorted_accessories)/6) 123 | for row in range(6): 124 | row_accessories = sorted_accessories[row*list_length:(row+1)*list_length] # Get the first group out of 6 125 | make_embed(embed, row_accessories, row+1) 126 | 127 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 128 | 129 | list_of_embeds.append(embed) 130 | 131 | await generate_static_preset_menu(ctx=ctx, list_of_embeds=list_of_embeds, emoji_list=EMOJI_LIST, is_response=is_response) 132 | 133 | -------------------------------------------------------------------------------- /bot/player_commands/populate_leaderboard.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | 4 | import requests 5 | from database_manager import insert_profile 6 | 7 | uuids = ['23331ef9edaa4e1fa71baa82bb1982c2', '4085ff0af28d4534bf325decb14376d5', '9648b72eeff6446588b13ce079c647a8', '130b3075c7b84ce0a016034c3a1d7bee', '2ed4b01e4f394d09b7ed66fcdfc3f727', 'c94f9e8412f94cfc88c06a1c6df3b0b5', '1545500448444122a9b750c1a856fb8c', '7c326e8b9a5a45b785ac79b110d718e3', '4fe0fdbfa5c6443e914ee786471f2453', '19605973f2554786af16e79692c4c4d1', '22fb74ab817945ffaefb23350b81487a', '4603e44959d24d1daf554b313b343f88', 'a68ae683e1684d49baf0966bf2537f5e', '9de64e171d034155b61f3f3056e69089', 'c672eed314f84dcaa0c6f74a5a52c8f2', '7da8f127c7b549eb9fd33f37b95252ac', 'a1c421ff5e024f75b1a46c30ca68c749', '773a891bd0ea44e583f487280892f439', 'ab4c7896b5c24d1fa60aad904b1fb6bb', '6e90365c2dc9458e97ce08534352e2a1', '6275c006ce154abc8143adc549ef76bb', '6fd1d52fcb0f4d97b8d84542f93f2658', 'b451e21fafda403f983da3842a55ab67', 'dd959a35c6d74ae28b9a7fcb9434637c', '1277d71f338046e298d90c9fe4055f00', '28667672039044989b0019b14a2c34d6', '529dabfd5b154d90b105a3a81da2c525', 'e23e44f605f24f23b65e51b449dd0c0d', 'fb3d96498a5b4d5b91b763db14b195ad', 'fa0ea97de33443d6b5045f18229a154b', '96b04cd0f41c4752801cc9baea6da892', '04cd3f240c7f42009b3398fba995c3a6', '4f562091f4614f659546717d6bbdfe79', '0291afe06e36412ab88cacafb74e1cd6', '88fec3c939904ec8a07890f342253d4d', '99f50870447c4e508c95086048a7f196', '39d7c75b5fbf437c8e853d58934c2d2b', 'd35de691f4ca4cc2bfe40f372f04a6e4', '885c77cebb29471b95e3516d1118bc76', '84deddf1f0c442079716f501b11db173', 'b3e7ed8fd4144e02a85c1a86d4012807', '0625fd8f36d746eeb7cb6a0972c0df0d', 'a05ac4b9445041fa8f29e3ea117958ad', '6329d4d93f04421ab95d356086fce83c', 'f1608634ce8a4e1c9645511069f59487', '78926764ee53437b91d96dbf2b3b1dca', '78acba393ce149f68855f9bca5907432', '6011e893c45542629c7c8cc94bd88a35', '5a9df7ad49aa4747939130efc3cd6cea', '5c6d8f6645b24a5ca37970053ffd32d6', '29135e50c229404ba0b2a147abc374fc', '236f99cc5439455cb10aa29c3202082e', 'c713e1ef28844ceaaac306c44e39505f', 'f5d20b3123b345b1a46cc9eb5a413d19', '6de0b8b24d8548e8ab1b545322909aee', '5435b597612f4554a3c651fd1c3ee96a', '1cb36e3f0b0742ea8abe396a70d234a4', '6808325e8d44418ca34b61e22471e5a7', 'e8ff41bc06b3449cbff3107e90309497', '2e9971217d894aa9beff00434843fe07', '4fef66aace5641b6b4b2bf272d765d9f', '29baf21ea788440da3f132b0a1ab01c3', 'd472f48813c941148c5a3c360e87c40b', '8a7ce3df6a4b4befad4e42028efe19cc', '90905cf347be483dbac440a6e631bc91', '41eb2439084e4233bab9b6defd7749fb', '973dc5a4f604489eb350aff3a07228a1', '9dad65f5d0ce4f6e83ff23ce676af28f', '86a7793c061743deb2e4894a5d2e5ccd', '66cb6384bbce477c854b815548458224', 'c558970dca7343a79083825bacfa65c7', 'cd5aa0c479394e8b812d1a755e450ef8', '9a5624746131475ea213b97f4d5e23a9', 'a68c92d3963942e3b1c695241fcfa2c0', '4ce231a79af8473faf1dab7ecf4de2ad', 'c0a5e970db87492e93c260f8221d1c98', '53ded400553046c694605f167df88366', '5ad6ed71fb17453c8e367cc66e8b6982', '3ffc65982a904193a8e17f241ddef6cd', '88687e0e24d940229868279d55616c26', 'b7ed1e5f6c624b57a21fa66f3b4e297a', '58e7ba62ed31445c8fa71cf5ed575940', '6f34fab4b6fe4522b215fa01ae68f412', 'd6b5d5120ef0409698ee2ad30754c7d2', '919e74ebf227422ea94719cb7273b03b', 'ded0c8298a9b4db0819822256a3eccb0', '6994c547f53e4107ace4a0bb48609bb5', '8cac680761a7412b91b306f489a5871f', '637c355ef3fc4eeb88c2a7c18c0bf815', 'dd5818337cf545e6b834ff1644b76cca', '311b651b782844a0a0889130922434ca', '2d17608169d844d0b8d828df1438dbfb', 'f5cddb7611cc45659e25448022ef81a1', '073a5abb4b734339a1e90ef43d504842', 'c00cc747e84340ddbf76374651b34c0b', 'c714874fdb594844b24f1f0c3b63408f', '7903f5c3631244ae9bc16018feb38b43', '23467d4464474adfb77c1e5a044e5e5b', 'f8afcbe455d242e1aaa6722d3ed110bc', '4995579a28f647dfbc03f33fd2aec058'] 8 | 9 | with open("text_files/hypixel_api_key.txt") as file: 10 | API_KEY = file.read() 11 | 12 | class populate_leaderboard_cog(commands.Cog): 13 | def __init__(self, bot) -> None: 14 | self.client = bot 15 | 16 | @commands.is_owner() 17 | @commands.command(aliases=["pl"]) 18 | async def populate_leaderboard(self, ctx) -> None: 19 | 20 | for uuid in uuids: 21 | print(f"Re-calculated {uuid}") 22 | profile_data = requests.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}").json() 23 | 24 | request = requests.post(f"http://{self.client.ip_address}:8000/pages/{uuid}", json=profile_data) 25 | if request.status_code != 200: 26 | continue 27 | 28 | request_data = request.json() 29 | 30 | data_totals = [request_data.get(page, {"total": 0})['total'] for page in ("purse", "banking", "inventory", "accessories", "ender_chest", "armor", "vault", "wardrobe", "storage", "pets")] 31 | insert_profile(uuid, request_data["profile_data"]["profile_name"], request_data["profile_data"]["profile_type"], *data_totals) 32 | 33 | -------------------------------------------------------------------------------- /bot/player_commands/price_check.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests # For making the api call 7 | 8 | from utils import error, hf, smarter_find_closest, bot_can_send, guild_ids 9 | from emojis import MATHS_EMOJIS 10 | 11 | 12 | class price_check_cog(commands.Cog): 13 | def __init__(self, bot) -> None: 14 | self.client = bot 15 | 16 | 17 | @commands.command(name="price_check", aliases=['price', 'p', 'pc']) 18 | async def price_check_command(self, ctx, *, input: Optional[str] = None) -> None: 19 | await self.price_check(ctx, input, is_response=False) 20 | 21 | @commands.slash_command(name="price_check", description="Gets the historic price data about an item", guild_ids=guild_ids) 22 | async def price_check_slash(self, ctx, input: Option(str, "input:", required=True)): 23 | if not bot_can_send(ctx): 24 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 25 | await self.price_check(ctx, input, is_response=True) 26 | 27 | async def price_check(self, ctx, input: Optional[str] = None, is_response: bool = False) -> None: 28 | closest = await smarter_find_closest(ctx, input, is_response=is_response) 29 | if closest is None: 30 | return 31 | 32 | if closest[0] == "enchant": 33 | enchant_type, level = closest[1].split(":") 34 | response = requests.get(f"https://sky.coflnet.com/api/item/price/ENCHANTED_BOOK?Enchantment={enchant_type}&EnchantLvl={level}").json() 35 | item_name = f"{enchant_type.replace('_', ' ').title()} {level} book" 36 | elif closest[0] == "pet": 37 | pet_type, rarity, level = closest[1].split(":") 38 | rarity_selector = "" if rarity == "None" else f"Rarity={rarity.upper()}" 39 | level_selector = "" if level == "None" else f"PetLevel={level}" 40 | if level != "None" and rarity != "None": 41 | level_selector = "&"+level_selector 42 | response = requests.get(f"https://sky.coflnet.com/api/item/price/PET_{pet_type.upper()}?{rarity_selector}{level_selector}").json() 43 | item_name = f"{pet_type.replace('_', ' ').title()}" 44 | elif closest[0] == "item": 45 | closest = closest[1] 46 | response = requests.get(f"https://sky.coflnet.com/api/item/price/{closest['internal_name']}").json() 47 | item_name = closest['name'] 48 | 49 | if "Slug" in response.keys() or "min" not in response.keys(): 50 | return await error(ctx, "Error, no items of that type could be found on the auction house!", "If you're searching for a pet, please end your search with 'pet', and if you're searching for an ultimate enchantment, please include `ultimate`.", is_response=is_response) 51 | 52 | string = [f"{MATHS_EMOJIS['min']} Minimum: {hf(response['min'])}", 53 | f"{MATHS_EMOJIS['max']} Maximum: {hf(response['max'])}", 54 | f"{MATHS_EMOJIS['volume']} Number sold: {hf(response['volume'])}", 55 | f"{MATHS_EMOJIS['median']} Median: {hf(response['median'])}", 56 | f"{MATHS_EMOJIS['mode']} Mode: {hf(response['mode'])}", 57 | f"{MATHS_EMOJIS['mean']} Mean Average: {hf(response['mean'])}", 58 | f"", 59 | f"Links: Definitions for mode, median and mean: [link](https://www.khanacademy.org/math/statistics-probability/summarizing-quantitative-data/mean-median-basics/a/mean-median-and-mode-review), API: [link](https://sky.coflnet.com/data)"] 60 | 61 | embed = discord.Embed(title=f"Price data found for {item_name}:", description="\n".join(string), colour=0x3498DB) 62 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 63 | if is_response: 64 | await ctx.respond(embed=embed) 65 | else: 66 | await ctx.send(embed=embed) 67 | 68 | -------------------------------------------------------------------------------- /bot/player_commands/rank.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | from database_manager import get_specific_networth_data, get_all_networth_data, get_sum_networth_data 7 | from emojis import PAGE_ICON_EMOJIS 8 | from parse_profile import input_to_uuid 9 | from utils import error, bot_can_send, guild_ids 10 | 11 | 12 | def get_percent_categories(uuid: str, user_data: dict) -> dict[str, float]: 13 | """ 14 | Returns the percentage that the uuid's group is less than, 100% = 0 money, 0.03% = Extremely rich 15 | """ 16 | data = get_all_networth_data() 17 | 18 | percent_categories = {} 19 | for i, category in enumerate(["purse", "banking", "inventory", "accessories", "ender chest", "armor", "vault", "wardrobe", "storage", "pets"]): 20 | # Get all purses from data, but only if they're less than the user's purse, then banking 21 | if user_data[i] < 1: 22 | continue 23 | filtered = [x[i] for x in data if x[i] < user_data[i]] 24 | percent_categories[category] = ((len(filtered)+1)/len(data))*100 25 | return percent_categories 26 | 27 | def overall_percent(uuid: str, user_data: dict) -> float: 28 | """ 29 | Returns the percentage that the total networth is less than 30 | """ 31 | user_total = sum(user_data) 32 | data = get_sum_networth_data() 33 | filtered = [x[0] for x in data if x[0] < user_total] 34 | return ((len(filtered)+1)/len(data))*100 35 | 36 | def fix(number_tuple: tuple) -> float: 37 | number = round(100-number_tuple[1], 3) 38 | return max(number, 0.01) 39 | 40 | ''' 41 | Linting stuff 42 | 60: error: Argument 2 to "get_percent_categories" has incompatible type "Tuple[Any, ...]"; expected "Dict[Any, Any]" 43 | 66: error: Argument 2 to "overall_percent" has incompatible type "Tuple[Any, ...]"; expected "Dict[Any, Any]" 44 | ''' 45 | 46 | class rank_cog(commands.Cog): 47 | def __init__(self, bot) -> None: 48 | self.client = bot 49 | 50 | @commands.command(name="rank") 51 | async def rank_command(self, ctx, provided_username: Optional[str] = None) -> None: 52 | await self.rank(ctx, provided_username, is_response=False) 53 | 54 | @commands.slash_command(name="rank", description="See how people's networth stacks up against everyone elses", guild_ids=guild_ids) 55 | async def rank_slash(self, ctx, username: Option(str, "username:", required=False)): 56 | if not bot_can_send(ctx): 57 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 58 | await self.rank(ctx, username, is_response=True) 59 | 60 | #========================================================================================================================== 61 | 62 | async def rank(self, ctx, provided_username: Optional[str] = None, is_response: bool = False) -> None: 63 | if is_response: 64 | await ctx.defer() 65 | # This tells discord that something is coming slightly later that instantly, 66 | # required because this command takes a little over a second to run 67 | 68 | player_data = await input_to_uuid(ctx, provided_username, is_response=is_response) 69 | if player_data is None: 70 | return None 71 | username, uuid = player_data 72 | 73 | user_data = get_specific_networth_data(uuid) 74 | if len(user_data) == 0: 75 | return await error(ctx, "Error, not enough data!", "We don't know how much your profile is worth right now, please use the networth command first!", is_response=is_response) 76 | 77 | categories = get_percent_categories(uuid, user_data[0]) 78 | sorted_data = sorted(categories.items(), key=lambda x: x[1], reverse=True) 79 | 80 | if len(sorted_data) < 4: 81 | return await error(ctx, "Error, not enough data!", "This person's API settings are to restrictive, or they have lots of empty containers.", is_response=is_response) 82 | 83 | total_networth_percentage = None, overall_percent(uuid, user_data[0]) 84 | 85 | string = [f"{PAGE_ICON_EMOJIS['overall']} Their overall networth is in the highest {fix(total_networth_percentage)}% of players.", 86 | f"", 87 | f"{PAGE_ICON_EMOJIS[sorted_data[0][0]]} For {sorted_data[0][0]}, they're in the top {fix(sorted_data[0])}% of players.", 88 | f"{PAGE_ICON_EMOJIS[sorted_data[1][0]]} For {sorted_data[1][0]}, they're in the top {fix(sorted_data[1])}% of players.", 89 | f"{PAGE_ICON_EMOJIS[sorted_data[2][0]]} For {sorted_data[2][0]}, they're in the top {fix(sorted_data[2])}% of players.", 90 | f"", 91 | f"{PAGE_ICON_EMOJIS[sorted_data[-1][0]]} For {sorted_data[-1][0]}, they're in the bottom {fix(sorted_data[-1])}% of players."] 92 | 93 | embed = discord.Embed(title=f"{username}'s stats:", description="\n".join(string), url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 94 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 95 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 96 | 97 | if is_response: 98 | await ctx.respond(embed=embed) 99 | else: 100 | await ctx.send(embed=embed) 101 | -------------------------------------------------------------------------------- /bot/player_commands/regenerate_leaderboard.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | 4 | import requests 5 | 6 | from database_manager import get_max_current_networth, insert_profile 7 | 8 | with open("text_files/hypixel_api_key.txt") as file: 9 | API_KEY = file.read() 10 | 11 | class regenerate_leaderboard_cog(commands.Cog): 12 | def __init__(self, bot) -> None: 13 | self.client = bot 14 | 15 | @commands.is_owner() 16 | @commands.command(aliases=["rl"]) 17 | async def regenerate_leaderboard(self, ctx) -> None: 18 | records = get_max_current_networth("regular") 19 | 20 | for uuid, _ in records: 21 | print(f"Re-calculated {uuid}") 22 | profile_data = requests.get(f"https://api.hypixel.net/skyblock/profiles?key={API_KEY}&uuid={uuid}").json() 23 | 24 | request = requests.post(f"http://{self.client.ip_address}:8000/pages/{uuid}", json=profile_data) 25 | if request.status_code != 200: 26 | continue 27 | 28 | request_data = request.json() 29 | 30 | data_totals = [request_data.get(page, {"total": 0})['total'] for page in ("purse", "banking", "inventory", "accessories", "ender_chest", "armor", "vault", "wardrobe", "storage", "pets")] 31 | insert_profile(uuid, request_data["profile_data"]["profile_name"], request_data["profile_data"]["profile_type"], *data_totals) 32 | 33 | -------------------------------------------------------------------------------- /bot/player_commands/set_prefix.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | from database_manager import * 7 | from utils import error 8 | 9 | class set_prefix_cog(commands.Cog): 10 | def __init__(self, bot) -> None: 11 | self.client = bot 12 | 13 | @commands.has_permissions(administrator=True) 14 | @commands.command(name="set_prefix", aliases=["setprefix"]) 15 | async def set_prefix_command(self, ctx, prefix: Optional[str] = None) -> None: 16 | 17 | is_response = False ############################################### 18 | 19 | print(f"------ Request made in from guild: {ctx.guild.id if ctx.guild is not None else 'DMs'}, from user: {ctx.author.id}, sometimes known as {ctx.author.display_name}\nThey set their guild/dm prefix to {prefix}") 20 | 21 | if prefix is None: 22 | return await error(ctx, "Command set_prefix must have a prefix", f"Example usage: `{ctx.prefix}set_prefix %`", is_response=is_response) 23 | if len(prefix) > 8: 24 | return await error(ctx, "Error, invalid prefix set!", "The prefix to start the community bot must be at most 8 characters long.", is_response=is_response) 25 | 26 | current_prefix = self.client.prefixes.get(f"{ctx.guild.id}", None) 27 | 28 | if current_prefix is None: 29 | set_guild_prefix(ctx.guild.id, prefix) 30 | else: 31 | update_guild_prefix(ctx.guild.id, prefix) 32 | 33 | self.client.prefixes[f"{ctx.guild.id}"] = prefix 34 | 35 | embed = discord.Embed(title=f"The prefix for Community Bot has been updated.", description=f"{ctx.author.display_name} has updated the prefix for community bot, it's now triggered by `{prefix}`", colour=0xe67e22) 36 | embed.set_footer(text=f"Use set_prefix to change the prefix for Community Bot") 37 | if is_response: 38 | await ctx.respond(embed=embed) 39 | else: 40 | await ctx.send(embed=embed) 41 | -------------------------------------------------------------------------------- /bot/player_commands/skills.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | from bisect import bisect 8 | 9 | from utils import error, hf, PROFILE_NAMES, bot_can_send, guild_ids, autocomplete_display_name 10 | from emojis import SKILL_EMOJIS 11 | from parse_profile import get_profile_data 12 | 13 | SKILLS = ['combat', 'foraging', 'mining', 'farming', 'fishing', 'alchemy', 'enchanting', 'taming', 'carpentry'] 14 | CUMULATIVE_XP_REQS = [50,175,375,675,1175,1925,2925,4425,6425,9925,14925,22425,32425,47425,67425,97425,147425,222425,322425,522425,822425,1222425,1722425,2322425,3022425,3822425,4722425,5722425,6822425,8022425,9322425,10722425,12222425,13822425,15522425,17322425,19222425,21222425,23322425,25522425,27822425,30222425,32722425,35322425,38072425,40972425,44072425,47472425,51172425,55172425,59472425,64072425,68972425,74172425,79672425,85472425,91572425,97972425,104972425,111672425] 15 | 16 | ##runecrafting_xp_requirements = [50, 150, 160, 2000, 250, 315, 400, 500, 625, 785, 1000, 1250, 1600, 2000, 2465, 3125, 4000, 5000, 6200, 7800, 9800, 12200, 15300] 17 | #runecrafting_cumul_xp_reqs = [0, 50,200,360,2360,2610,2925,3325,3825,4450,5235,6235,7485,9085,11085,13550,16675,20675,25675,31875,39675,49475] 18 | 19 | max_levels = { 20 | "farming": 60, 21 | "mining": 60, 22 | "combat": 60, 23 | "foraging": 50, 24 | "fishing": 50, 25 | "enchanting": 60, 26 | "alchemy": 50, 27 | "taming": 50, 28 | "carpentry": 50, 29 | "runecrafting": 25, 30 | } 31 | 32 | level_squares = ["⬛", "⬜", "🟩", "🟦", "🟪", "🟨", "<:pink_square:1073051068998623342>", "🟦", "🟥", "[🔴]"] 33 | 34 | def get_level(skill_data: dict, skill: str) -> int: 35 | return min(bisect(CUMULATIVE_XP_REQS, skill_data.get(f'experience_skill_{skill}', 0)), max_levels[skill]) 36 | 37 | class skills_cog(commands.Cog): 38 | def __init__(self, bot) -> None: 39 | self.client = bot 40 | 41 | @commands.command(name="skills", aliases=['skill']) 42 | async def skills_command(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None) -> None: 43 | await self.skills(ctx, provided_username, provided_profile_name, is_response=False) 44 | 45 | @commands.slash_command(name="skills", description="Gets a players skills", guild_ids=guild_ids) 46 | async def skills_slash(self, ctx, username: Option(str, "username:", required=False, autocomplete=discord.utils.basic_autocomplete(values=["red", "green", "blue"])), 47 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 48 | if not bot_can_send(ctx): 49 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 50 | await self.skills(ctx, username, profile, is_response=True) 51 | 52 | async def skills(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool = False) -> None: 53 | 54 | player_data: Optional[dict] = await get_profile_data(ctx, provided_username, provided_profile_name, is_response=is_response) 55 | if player_data is None: 56 | return 57 | username = player_data["username"] 58 | 59 | skill_data = player_data 60 | 61 | embed = discord.Embed(title=f"{username}", url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 62 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 63 | 64 | total_skill_xp = sum(skill_data.get(f'experience_skill_{skill}', 0) for skill in SKILLS) 65 | total_counted_levels = sum(get_level(skill_data, skill) for skill in SKILLS) 66 | skill_average = round(total_counted_levels/len(SKILLS), 2) 67 | 68 | skyblock_level, skyblock_level_overflow = divmod(int(skill_data["leveling"]["experience"]), 100) 69 | 70 | embed.add_field(name=f"Skills Data:", value=f"Total Skill XP: **{hf(total_skill_xp)}**\nSkill Average: **{hf(skill_average)}**", inline=True) 71 | embed.add_field(name=f"Skyblock Level:", value=f"Level {skyblock_level} - {level_squares[skyblock_level//40]}\n[{skyblock_level_overflow}/100]", inline=True) 72 | embed.add_field(name=f"\u200b", value="\u200b", inline=True) 73 | 74 | for skill in SKILLS: 75 | cumulative_xp = int(skill_data.get(f'experience_skill_{skill}', 0)) 76 | current_level = get_level(skill_data, skill) 77 | if current_level >= max_levels[skill]: 78 | amount_in = cumulative_xp-CUMULATIVE_XP_REQS[max_levels[skill]-1] 79 | next_level_requirements = 0 80 | progress = "MAX" 81 | else: 82 | amount_in = cumulative_xp-CUMULATIVE_XP_REQS[current_level-1] 83 | next_level_requirements = CUMULATIVE_XP_REQS[current_level]-CUMULATIVE_XP_REQS[current_level-1] 84 | progress = f"{round((amount_in/next_level_requirements)*100, 2)}%" 85 | 86 | #print(f"Level {current_level}, Next Level Reqs: {next_level_requirements}, Amount in: {amount_in}, progress: {progress}") 87 | embed.add_field(name=f"{SKILL_EMOJIS[skill]} {skill.title()} ({current_level})", value=f"**{hf(amount_in)}**/{hf(next_level_requirements)}\nTotal XP: **{hf(cumulative_xp)}**\nProgress: **{progress}**", inline=True) 88 | 89 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 90 | if is_response: 91 | await ctx.respond(embed=embed) 92 | else: 93 | await ctx.send(embed=embed) 94 | -------------------------------------------------------------------------------- /bot/player_commands/sky.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | from parse_profile import input_to_uuid 7 | from utils import bot_can_send, guild_ids 8 | 9 | class sky_cog(commands.Cog): 10 | def __init__(self, bot) -> None: 11 | self.client = bot 12 | 13 | @commands.command(name="sky") 14 | async def sky_command(self, ctx, username: Optional[str] = None) -> None: 15 | await self.sky(ctx, username, is_response=False) 16 | 17 | @commands.slash_command(name="sky", description="Gets the sky link of the target", guild_ids=guild_ids) 18 | async def sky_slash(self, ctx, username: Option(str, "username:", required=False)): 19 | if not bot_can_send(ctx): 20 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 21 | await self.sky(ctx, username, is_response=True) 22 | 23 | async def sky(self, ctx, provided_username: Optional[str] = None, is_response: bool = False) -> None: 24 | player_data: Optional[tuple[str, str]] = await input_to_uuid(ctx, provided_username, is_response=is_response) 25 | if player_data is None: 26 | return None 27 | username, uuid = player_data 28 | 29 | if is_response: 30 | await ctx.respond(f"https://sky.shiiyu.moe/stats/{username}") # Send the link with the target's name 31 | else: 32 | await ctx.send(f"https://sky.shiiyu.moe/stats/{username}") # Send the link with the target's name 33 | -------------------------------------------------------------------------------- /bot/player_commands/slayer.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | from bisect import bisect 8 | 9 | from utils import error, hf, PROFILE_NAMES, bot_can_send, guild_ids 10 | from emojis import SLAYER_EMOJIS 11 | from parse_profile import get_profile_data 12 | 13 | #===================== 14 | slayer_level_requirements = { 15 | 'zombie': [5, 15, 200, 1000, 5000, 20000, 100000, 400000, 1000000], 16 | 'spider': [5, 25, 200, 1000, 5000, 20000, 100000, 400000, 1000000], 17 | 'wolf': [10, 30, 250, 1500, 5000, 20000, 100000, 400000, 1000000], 18 | 'enderman': [10, 30, 250, 1500, 5000, 20000, 100000, 400000, 1000000], 19 | } 20 | 21 | SLAYER_COST = [2_000, 7_500, 20_000, 50_000, 100_000] # Same for all of them 22 | 23 | BOSSES = list(SLAYER_EMOJIS.keys()) # For doing [:3] 24 | 25 | #===================== 26 | def get_mob_data(mob, slayer_bosses): 27 | xp = slayer_bosses[mob].get('xp', 0) 28 | level = bisect(slayer_level_requirements[mob], xp) 29 | next_level_xp = 0 if level > 8 else slayer_level_requirements[mob][min(level, 8)] 30 | progress = "MAX" if level > 8 else f"{round((xp/next_level_xp)*100, 2)}%" 31 | return level, xp, next_level_xp, progress 32 | 33 | def add_mob_table(mob, slayer_bosses): 34 | return "```scala\n"+"\n".join(f"Tier {x+1}: "+str(slayer_bosses[mob].get(f"boss_kills_tier_{x}", 0)) for x in range(5))+"```" 35 | #===================== 36 | class slayer_cog(commands.Cog): 37 | def __init__(self, bot) -> None: 38 | self.client = bot 39 | 40 | @commands.command(name="slayer", aliases=['slayers', 'slay']) 41 | async def slayer_command(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None) -> None: 42 | await self.get_slayer(ctx, provided_username, provided_profile_name, is_response=False) 43 | 44 | @commands.slash_command(name="slayer", description="Gets slayer data about someone", guild_ids=guild_ids) 45 | async def slayer_slash(self, ctx, username: Option(str, "username:", required=False), 46 | profile: Option(str, "profile", choices=PROFILE_NAMES, required=False)): 47 | if not bot_can_send(ctx): 48 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 49 | await self.get_slayer(ctx, username, profile, is_response=True) 50 | 51 | @commands.user_command(name="Get slayer data", guild_ids=guild_ids) 52 | async def slayer_context_menu(self, ctx, member: discord.Member): 53 | if not bot_can_send(ctx): 54 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 55 | await self.get_slayer(ctx, member.display_name, None, is_response=True) 56 | 57 | 58 | async def get_slayer(self, ctx, provided_username: Optional[str] = None, provided_profile_name: Optional[str] = None, is_response: bool=False) -> None: 59 | player_data: Optional[dict] = await get_profile_data(ctx, provided_username, provided_profile_name, is_response=is_response) 60 | if player_data is None: 61 | return 62 | username = player_data["username"] 63 | 64 | slayer_bosses = player_data["slayer_bosses"] 65 | #===================== 66 | total_slayer_xp = sum(slayer_bosses.get(mob).get('xp', 0) for mob in BOSSES) 67 | #===================== 68 | total_paid = 0 69 | for i in range(5): 70 | for mob in BOSSES: 71 | total_paid += SLAYER_COST[i]*slayer_bosses.get(mob).get(f"boss_kills_tier_{i}", 0) 72 | #===================== 73 | embed = discord.Embed(title=f"{username}", url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 74 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 75 | #===================== 76 | embed.add_field(name="Slayer Data", value=f"Total Slayer XP: **{hf(total_slayer_xp)}**\nSlayer Coins Spent: **{hf(total_paid)}**", inline=False) 77 | #===================== 78 | for mob in BOSSES[:3]: 79 | current_level, current_xp, next_level_xp, progress = get_mob_data(mob, slayer_bosses) 80 | embed.add_field(name=f"{SLAYER_EMOJIS[mob]} {mob.title()} ({current_level})", value=f"**{hf(current_xp)}**/{hf(next_level_xp)}\nProgress: **{progress}**", inline=True) # Total XP: (**{hf(current_xp)}**)\n 81 | 82 | for mob in BOSSES[:3]: 83 | string = add_mob_table(mob, slayer_bosses) 84 | embed.add_field(name="Boss Kills:", value=string, inline=True) 85 | 86 | # Because endermen make this annoying 87 | current_level, current_xp, next_level_xp, progress = get_mob_data('enderman', slayer_bosses) 88 | embed.add_field(name=f"{SLAYER_EMOJIS['enderman']} Enderman ({current_level})", value=f"**{hf(current_xp)}**/{hf(next_level_xp)}\nProgress: **{progress}**", inline=True) 89 | 90 | embed.insert_field_at(index=8, name='\u200b', value='\u200b', inline=True) 91 | embed.insert_field_at(index=9, name='\u200b', value='\u200b', inline=True) 92 | 93 | string = add_mob_table('enderman', slayer_bosses) 94 | embed.add_field(name="Boss Kills:", value=string, inline=True) 95 | 96 | embed.insert_field_at(index=11, name='\u200b', value='\u200b', inline=True) 97 | embed.insert_field_at(index=12, name='\u200b', value='\u200b', inline=True) 98 | #===================== 99 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 100 | if is_response: 101 | await ctx.respond(embed=embed) 102 | else: 103 | await ctx.send(embed=embed) 104 | -------------------------------------------------------------------------------- /bot/player_commands/weights.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | import requests 7 | 8 | from utils import error, hf, WEIGHT_API_KEY, bot_can_send, guild_ids 9 | from parse_profile import input_to_uuid 10 | from menus import generate_static_preset_menu 11 | from emojis import SKILL_EMOJIS, CLASS_EMOJIS, SLAYER_EMOJIS 12 | 13 | EMOJI_DICT = {**SKILL_EMOJIS, **CLASS_EMOJIS} 14 | EMOJI_DICT["revenant"] = SLAYER_EMOJIS["zombie"] 15 | EMOJI_DICT["tarantula"] = SLAYER_EMOJIS["spider"] 16 | EMOJI_DICT["sven"] = SLAYER_EMOJIS["wolf"] 17 | EMOJI_DICT["enderman"] = SLAYER_EMOJIS["enderman"] 18 | 19 | PAGE_URLS = {"dungeons": ["healer", "mage", "berserker", "archer", "tank"], 20 | "skills": ["mining", "foraging", "enchanting", "farming", "combat", "fishing", "alchemy", "taming", "carpentry"], 21 | "slayers": ["revenant", "tarantula", "sven", "enderman"], 22 | } 23 | 24 | EMOJI_LIST = ["<:paper:1035458893482037278>", "<:dungeons:1035458870363041793>", "<:skills:1035458899240816702>", 25 | "<:slayer:1035458900985651222>", "<:misc:854801277489774613>"] 26 | 27 | class weights_cog(commands.Cog): 28 | def __init__(self, bot) -> None: 29 | self.client = bot 30 | 31 | @commands.command(name="weight", aliases=['weights', 'w', 'waits']) 32 | async def weight_command(self, ctx, provided_username: Optional[str] = None) -> None: 33 | await self.get_weights(ctx, provided_username, is_response=False) 34 | 35 | @commands.slash_command(name="weight", description="Gets someone's profile weight", guild_ids=guild_ids) 36 | async def weight_slash(self, ctx, username: Option(str, "username:", required=False)): 37 | if not bot_can_send(ctx): 38 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 39 | await self.get_weights(ctx, username, is_response=True) 40 | 41 | @commands.user_command(name="Get profile weight", guild_ids=guild_ids) 42 | async def weight_context_menu(self, ctx, member: discord.Member): 43 | if not bot_can_send(ctx): 44 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 45 | await self.get_weights(ctx, member.display_name, is_response=True) 46 | 47 | async def get_weights(self, ctx, provided_username: Optional[str] = None, is_response: bool = False): 48 | player_data: Optional[tuple[str, str]] = await input_to_uuid(ctx, provided_username, is_response=is_response) 49 | if player_data is None: 50 | return None 51 | username, uuid = player_data 52 | 53 | #==================================================================================== 54 | # Main page 55 | response = requests.get(f"https://hypixel-api.senither.com/v1/profiles/{uuid}/weight?key={WEIGHT_API_KEY}").json() 56 | if response["status"] != 200: 57 | return await error(ctx, "Error, the api couldn't fufill this request.", "As this is an external API, CommunityBot cannot fix this for now. Please try again later.", is_response=is_response) 58 | response = response["data"] 59 | 60 | total_regular_weight = round(response["weight"], 2) 61 | total_overflow_weight = round(response["weight_overflow"], 2) 62 | 63 | list_of_elems = [f"Total Regular Weight: **{total_regular_weight}**", 64 | f"Total Overflow Weight: **{total_overflow_weight}**", 65 | f"Total Weight: **{round(total_regular_weight+total_overflow_weight, 2)}**", 66 | "", 67 | "Click the buttons to start!", 68 | "<:dungeons:864588623394897930> Dungeons", 69 | "<:skills:864588638066311200> Skills", 70 | "<:slayers:864588648111276072> Slayer", 71 | "<:misc:854801277489774613> Info"] 72 | 73 | embed = discord.Embed(title=f"Weights Calculator For {username}:", description="\n".join(list_of_elems), 74 | url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 75 | 76 | list_of_embeds = [embed] 77 | #==================================================================================== 78 | # Skills, slayer and dungeons 79 | for page in ["dungeons", "skills", "slayers"]: 80 | data = response[page] 81 | if data is None: 82 | embed = discord.Embed(title=f"{page.title()} weights for {username}:", description=f"There doesn't seem to be anything here?\nThis is most likely because {username} hasn't done any dungeons before.", 83 | url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 84 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 85 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 86 | list_of_embeds.append(embed) 87 | continue 88 | 89 | if page == "skills": 90 | description_start = f"Skill average: **{round(data['average_skills'], 2)}**" 91 | elif page == "slayers": 92 | description_start = f"Total coins spent: **{hf(data['total_coins_spent'])}**" 93 | elif page == "dungeons": 94 | description_start = f"Secrets found: **{data['secrets_found']}**" 95 | 96 | embed = discord.Embed(title=f"{page.title()} weights for {username}:", description=f"Total {page.removesuffix('s')} weight: **{round(data['weight']+data['weight_overflow'], 2)}**\n{description_start}", 97 | url=f"https://sky.shiiyu.moe/stats/{username}", colour=0x3498DB) 98 | 99 | if page == "dungeons": 100 | catacombs_weight = round(data['types']['catacombs']['weight'], 2) 101 | catacombs_overflow = round(data['types']['catacombs']['weight_overflow'], 2) 102 | embed.add_field(name=f"{EMOJI_DICT['catacombs']} Cata ({int(data['types']['catacombs']['level'])})", 103 | value=f"Regular: **{catacombs_weight}**\nOverflow: **{catacombs_overflow}**\nTotal: **{round(catacombs_weight+catacombs_overflow, 2)}**", inline=True) 104 | 105 | remapped_data = data.get("bosses", None) or data.get("classes", None) or data # Remap data to be the sub list. 106 | 107 | for category in PAGE_URLS[page]: 108 | level = int(remapped_data[category]["level"]) 109 | regular = round(remapped_data[category]["weight"], 2) 110 | overflow = round(remapped_data[category]["weight_overflow"], 2) 111 | embed.add_field(name=f"{EMOJI_DICT[category]} {category.title()} ({level})", 112 | value=f"Regular: **{regular}**\nOverflow: **{overflow}**\nTotal: **{round(regular+overflow, 2)}**", inline=True) 113 | 114 | list_of_embeds.append(embed) 115 | #==================================================================================== 116 | # Info page 117 | embed = discord.Embed(title=f"Info page", description=f"Weights are a concept that attempts to represent how far into the game you are, whether that be in slayer, dungeons, or your skills. It uses an extensive formula to calculate the weights. The formula and the data, however, is provided by the Senither API [found here](https://hypixel-api.senither.com/), so no changes can be made to it.\n\nFor a rough idea of how it's calculated, each skills/slayer/dungeon level has a specific number that decides how important to classify that level, and any level above max level will get diminishing returns.", colour=0x3498DB) 118 | list_of_embeds.append(embed) 119 | #==================================================================================== 120 | for i, embed in enumerate(list_of_embeds): 121 | embed.set_thumbnail(url=f"https://mc-heads.net/head/{username}") 122 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 123 | list_of_embeds[i] = embed 124 | 125 | await generate_static_preset_menu(ctx=ctx, list_of_embeds=list_of_embeds, emoji_list=EMOJI_LIST, is_response=is_response) 126 | 127 | -------------------------------------------------------------------------------- /bot/player_commands/wiki.py: -------------------------------------------------------------------------------- 1 | import discord # type: ignore 2 | from discord.ext import commands # type: ignore 3 | from discord.commands import Option # type: ignore 4 | from typing import Optional 5 | 6 | from utils import error, ITEMS, bot_can_send, guild_ids 7 | from difflib import SequenceMatcher 8 | 9 | class wiki_cog(commands.Cog): 10 | def __init__(self, bot) -> None: 11 | self.client = bot 12 | 13 | @commands.command(name="wiki", aliases=['wiki_link', 'wiki_links']) 14 | async def wiki_command(self, ctx, *, item: Optional[str] = None) -> None: 15 | await self.wiki(ctx, item, is_response=False) 16 | 17 | @commands.slash_command(name="wiki", description="Gets the wiki entry for an item", guild_ids=guild_ids) 18 | async def wiki_slash(self, ctx, item: Option(str, "item:", required=True)): 19 | if not bot_can_send(ctx): 20 | return await ctx.respond("You're not allowed to do that here.", ephemeral=True) 21 | await self.wiki(ctx, item, is_response=True) 22 | 23 | async def wiki(self, ctx, user_input: Optional[str] = None, is_response: bool = False) -> None: 24 | 25 | if user_input is None: 26 | return await error(ctx, "No item given.", f"Please give the item you want to find the wiki page on.\nExample usage: {ctx.prefix}wiki Talisman Of Coins", is_response=is_response) 27 | 28 | # Get the closest match between your input and all the names 29 | closest = max(ITEMS.values(), key=lambda item: SequenceMatcher(None, user_input.lower(), item["name"].lower()).ratio()) 30 | 31 | if SequenceMatcher(None, user_input.lower(), closest["name"].lower()).ratio() < 0.6: 32 | # No item was found 33 | return await error(ctx, "No wiki entry with the provided input!", "Try being more accurate, and exclude special characters.", is_response=is_response) 34 | 35 | # Everything is fine, send it 36 | embed = discord.Embed(title=f"Wiki entry for {closest['name']}:", description=f"You can find the wiki entry [here]({closest['wiki_link']}).", colour=0x3498DB) 37 | embed.set_footer(text=f"Command executed by {ctx.author.display_name} | Community Bot. By the community, for the community.") 38 | if is_response: 39 | await ctx.respond(embed=embed) 40 | else: 41 | await ctx.send(embed=embed) 42 | -------------------------------------------------------------------------------- /bot/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.25.1 2 | mysql.connector==2.2.9 -------------------------------------------------------------------------------- /bot/test_cog.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord.commands import Option 4 | from discord.ui import InputText, Modal 5 | 6 | class MyModal(Modal): 7 | def __init__(self, *args, **kwargs) -> None: 8 | super().__init__(*args, **kwargs) 9 | self.add_item( 10 | InputText( 11 | label="Short Input", 12 | placeholder="Placeholder Test" 13 | ) 14 | ) 15 | print("Here") 16 | 17 | self.add_item( 18 | InputText( 19 | label="Longer Input", 20 | value="Longer Value\nSuper Long Value", 21 | style=discord.InputTextStyle.long, 22 | placeholder="Placeholder", 23 | row=0 24 | ) 25 | ) 26 | print("Here2") 27 | 28 | async def callback(self, interaction): 29 | embed = discord.Embed(title="Your Modal Results") 30 | embed.add_field(name="First Input", value=self.children[0].value, inline=False) 31 | embed.add_field(name="Second Input", value=self.children[1].value, inline=False) 32 | await interaction.response.send_message(embeds=[embed]) 33 | 34 | class test_cog(commands.Cog): 35 | def __init__(self, bot) -> None: 36 | self.client = bot 37 | 38 | ''' 39 | @commands.slash_command(name="test", description="test_test", guild_ids=[854749884103917599]) 40 | async def test_slash(self, ctx, username: Option(str, "username:", required=False, autocomplete=discord.utils.basic_autocomplete(values=["red", "green", "blue"])), 41 | profile: Option(str, "profile", choices=["Apple", "Banana"], required=False)): 42 | print(username, profile, is_response) 43 | ''' 44 | @commands.slash_command(name="test", description="test_test", guild_ids=[854749884103917599]) # CB 45 | async def test_slash(self, ctx): 46 | #embed = discord.Embed(description="Testing modalds", colour=0x3498DB) 47 | #await embed.send() 48 | print("We're here") 49 | 50 | modal = MyModal(title="Slash Command Modal") 51 | await ctx.interaction.response.send_modal(modal) 52 | 53 | 54 | -------------------------------------------------------------------------------- /bot/zz check for.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | find = "<:minions" 4 | 5 | folders = [f.path for f in os.scandir(".") if f.is_dir()]+[""] 6 | 7 | def find_it(folder, ext): 8 | for file in folder: 9 | if file.endswith(".py") and file != "zz check for.py": 10 | with open(ext+file, "r", encoding="utf-8") as a: 11 | try: 12 | for line in a.readlines(): 13 | if find.lower() in line.lower(): 14 | print(" > Found in", file, " contained "+find) 15 | #print(" > ", line) 16 | except Exception as e: 17 | print(e) 18 | pass 19 | 20 | for folder in folders: 21 | if folder in [".\__pycache__", ".\.mypy_cache", '.\dead_code']: 22 | continue 23 | print("Looking in folder:", folder) 24 | if folder == "": 25 | find_it(os.listdir(), "") 26 | else: 27 | find_it(os.listdir(folder), folder+"/") 28 | -------------------------------------------------------------------------------- /info/how_things_are_calculated.md: -------------------------------------------------------------------------------- 1 | The networth calculations aren't complicated, but are made up from many different data points. 2 | The API will take the compressed GZIP data and decode it into it's different containers. 3 | It will then process each item in the container, which is then also split into 3 different sections, base items, pets, and enchanted books. 4 | It may look for, for example, if the item has a recombobulator 3000, and if it does, increase the value of that item. 5 | 6 | **Here's what the code currently does to get the value.\** 7 | It gets a collection of items, from your: 8 | - Accessory bag 9 | - Inventory 10 | - Ender chest 11 | - Currently equipped armor 12 | - Wardrobe 13 | - Personal vault 14 | - Storage 15 | - Pets 16 | 17 | With these (excluding pets), it will do the following: 18 | 1. If the item is purchasable from an NPC then: 19 | If the price of the item from the NPC is less than the BIN, set that as it's base price, 20 | If the price of the item from the NPC is more than the BIN, set the price to the BIN value. 21 | 2. If it's purchasable from the Bazaar, use that price, it's the most accurate. 22 | 3. If it's purchasable on the auction house, use the lowest "Buy It Now", this is reasonably accurate. 23 | 4. If it's not on either of the above, check Jerry's Price list, it's not always entirely accurate, but has much more items. 24 | 25 | Then we calculate some different price points for base items: 26 | - If it's a Theoretical hoe, 1,000,000 for a Mathematical Blueprint, plus the cost of materials to get it to it's tier (e.g. Tier 3) 27 | - If it has hot potato books, add the price of a HPB for each book, and the price of a FPB for each fuming potato book. 28 | - If it's been recombobulated, add the price of a recombobulator 3000 from Bazzar to the value. 29 | - If it's a dungeon item, give the value in essence for each item (and each star), including the price of the Master Stars. 30 | - It it has a reforge stone applied to it, calculate the cost of the reforge stone, as well as the cost to apply it. 31 | - If it has a talisman enrichment, add the price of that enrichment from BIN. 32 | - If it's a Wooden Sword with a "Wood Singularity" added to it, add the price of that item from the BIN. 33 | - If the item is armor and has a skin on it, add the price of that skin from BIN. 34 | - If it has an "Art Of War" book added to it, add the price of that book from the BIN to the value. 35 | - If it has any "Farming for Dummies" books, add them to the value. 36 | - For each enchantment, if it's from the enchantment table, use a base value, if not, if it's not on BIN, try doing 2 of the level below, or 4 of two levels below, etc. This excludes: Compact, Expertise and Cultivating. 37 | - If the item is a drill, add the cost of each upgrade from the BIN to the value. 38 | - If it has scrolls (e.g. on a Hyperion), add the price of all of them to the value. 39 | - If it's got Transmission Tuners, add the value from BIN of each one to the value. 40 | - If it's an Aspect of the Void, and is Ethermerged, add the value of the Etherwarp merger and Conduit to the value. 41 | - If it's a Midas Staff or Sword, add the amount of the winning bid to it. 42 | - If it has gems on it, add the cost of all the gems from bazaar. 43 | - If it has Gemstone chambers, add the cost of each gemstone chamber from BIN. 44 | - If it has gemstone power scrolls, add the cost of the power scrolls from BIN. 45 | 46 | For pets: 47 | - Calculate the price of the pet from BIN (for that tier). 48 | - Add the cost of the pet's held item from BIN. 49 | - Add the cost of the pet's skin from BIN. 50 | - We add the amount of pet xp by 0.2, to get it's level's value *(this is partially subjective). 51 | - For Golden Dragons, we calculate the same as above, but each level above 100 is 3x the xp, rather than 0.2 (this is surprisingly accurate) 52 | -------------------------------------------------------------------------------- /info/privacy_policy.md: -------------------------------------------------------------------------------- 1 | This privacy policy will help you understand how CommunityBot ('we') uses and protects the data you provide to us when you use 2 | CommunityBot. 3 | We reserve the right to change this policy at any given time, of which you can see this updated changes here. If you want to make sure that you are up to date with the 4 | latest changes, we advise you to trequently visit this page. 5 | **What User Data We Collect** 6 | _When you use CommunityBot, we may collect the following data:_ 7 | Your discord messages, including media and attachments 8 | _Why We Collect Your Data_ 9 | The bot supports logging, which helps with moderation. This helps keep users safe and keeps everyone accountable for the rules. 10 | _Restricting the Collection of your Personal Data_ 11 | At some point, you might wish to restrict the use and collection of your personal data. You can achieve this by doing the following: 12 | The data we collect by logging can be deleted by anyone with "Manage Messages" permissions on the server, and you can opt out of being logged by using the "stop_tracking" command (prefix is server dependant). Alternatively, message Skezza#1139 to have you manually removed from the bot's logging in the future. 13 | -------------------------------------------------------------------------------- /info/simple_example.js: -------------------------------------------------------------------------------- 1 | console.log("Coming soon!") -------------------------------------------------------------------------------- /info/simple_example.py: -------------------------------------------------------------------------------- 1 | print("Coming soon!") 2 | -------------------------------------------------------------------------------- /info/terms_of_service.md: -------------------------------------------------------------------------------- 1 | You may not use this bot for anything harmful, and it's purpose is purely to aid convenience to the user. Any other uses are prohibited. 2 | --------------------------------------------------------------------------------