├── .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 ·   [](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 |
--------------------------------------------------------------------------------