mikubot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mikubot-0.1.0/PKG-INFO +57 -0
- mikubot-0.1.0/README.md +21 -0
- mikubot-0.1.0/pyproject.toml +48 -0
- mikubot-0.1.0/src/mikubot/__init__.py +16 -0
- mikubot-0.1.0/src/mikubot/commands/__init__.py +21 -0
- mikubot-0.1.0/src/mikubot/commands/avatar.py +22 -0
- mikubot-0.1.0/src/mikubot/commands/clap.py +19 -0
- mikubot-0.1.0/src/mikubot/commands/code.py +56 -0
- mikubot-0.1.0/src/mikubot/commands/edenpatch.py +20 -0
- mikubot-0.1.0/src/mikubot/commands/gamebanana.py +115 -0
- mikubot-0.1.0/src/mikubot/commands/goofy_translate.py +75 -0
- mikubot-0.1.0/src/mikubot/commands/guess.py +202 -0
- mikubot-0.1.0/src/mikubot/commands/it.py +29 -0
- mikubot-0.1.0/src/mikubot/commands/list_brazil.py +14 -0
- mikubot-0.1.0/src/mikubot/commands/purge.py +28 -0
- mikubot-0.1.0/src/mikubot/commands/rename_channel.py +215 -0
- mikubot-0.1.0/src/mikubot/commands/reply_commands.py +23 -0
- mikubot-0.1.0/src/mikubot/commands/role.py +20 -0
- mikubot-0.1.0/src/mikubot/commands/settings.py +26 -0
- mikubot-0.1.0/src/mikubot/commands/stop.py +30 -0
- mikubot-0.1.0/src/mikubot/commands/sync.py +19 -0
- mikubot-0.1.0/src/mikubot/commands/toggle_bot_response.py +21 -0
- mikubot-0.1.0/src/mikubot/commands/user_count.py +20 -0
- mikubot-0.1.0/src/mikubot/commands/utils.py +28 -0
- mikubot-0.1.0/src/mikubot/commands/uwu.py +32 -0
- mikubot-0.1.0/src/mikubot/commands/you_are_going_to_brazil.py +28 -0
- mikubot-0.1.0/src/mikubot/commands/zoe.py +138 -0
- mikubot-0.1.0/src/mikubot/config.py +336 -0
- mikubot-0.1.0/src/mikubot/discord.py +351 -0
- mikubot-0.1.0/src/mikubot/logger.py +65 -0
- mikubot-0.1.0/src/mikubot/modals/__init__.py +8 -0
- mikubot-0.1.0/src/mikubot/modals/base.py +35 -0
- mikubot-0.1.0/src/mikubot/modals/brazil.py +45 -0
- mikubot-0.1.0/src/mikubot/modals/gamebanana.py +34 -0
- mikubot-0.1.0/src/mikubot/modals/general.py +13 -0
- mikubot-0.1.0/src/mikubot/modals/rename_chat.py +114 -0
- mikubot-0.1.0/src/mikubot/modals/trigger_words.py +50 -0
- mikubot-0.1.0/src/mikubot/modals/uwufy.py +52 -0
- mikubot-0.1.0/src/mikubot/modals/zoe.py +40 -0
- mikubot-0.1.0/src/mikubot/stocks/__init__.py +12 -0
- mikubot-0.1.0/src/mikubot/stocks/command/__init__.py +880 -0
- mikubot-0.1.0/src/mikubot/stocks/command/embeds.py +207 -0
- mikubot-0.1.0/src/mikubot/stocks/command/modals.py +141 -0
- mikubot-0.1.0/src/mikubot/stocks/command/utils.py +11 -0
- mikubot-0.1.0/src/mikubot/stocks/command/views.py +225 -0
- mikubot-0.1.0/src/mikubot/stocks/core.py +311 -0
- mikubot-0.1.0/src/mikubot/stocks/derivative_rules/__init__.py +6 -0
- mikubot-0.1.0/src/mikubot/stocks/derivative_rules/asymmetric.py +30 -0
- mikubot-0.1.0/src/mikubot/stocks/derivative_rules/base.py +32 -0
- mikubot-0.1.0/src/mikubot/stocks/derivative_rules/decay.py +40 -0
- mikubot-0.1.0/src/mikubot/stocks/derivative_rules/knock_out.py +33 -0
- mikubot-0.1.0/src/mikubot/stocks/graphs.py +195 -0
- mikubot-0.1.0/src/mikubot/stocks/history.py +163 -0
- mikubot-0.1.0/src/mikubot/stocks/intervals.py +64 -0
- mikubot-0.1.0/src/mikubot/stocks/links.py +128 -0
- mikubot-0.1.0/src/mikubot/stocks/metrics/__init__.py +3 -0
- mikubot-0.1.0/src/mikubot/stocks/metrics/liquidity.py +104 -0
- mikubot-0.1.0/src/mikubot/stocks/metrics/prices.py +45 -0
- mikubot-0.1.0/src/mikubot/stocks/metrics/volatility.py +105 -0
- mikubot-0.1.0/src/mikubot/stocks/models.py +375 -0
- mikubot-0.1.0/src/mikubot/stocks/polymorphic.py +68 -0
- mikubot-0.1.0/src/mikubot/stocks/securities.py +52 -0
- mikubot-0.1.0/src/mikubot/stocks/store.py +90 -0
- mikubot-0.1.0/src/mikubot/stocks/transactions.py +164 -0
- mikubot-0.1.0/src/mikubot/stocks/utils.py +20 -0
- mikubot-0.1.0/src/mikubot/utils.py +14 -0
mikubot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mikubot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Discord bot used for the Eden-Project Discord server.
|
|
5
|
+
Keywords: discord,eden,miku,vocaloid,project diva
|
|
6
|
+
Author: Jay184
|
|
7
|
+
Author-email: Jay184 <me@jay0.mozmail.com>
|
|
8
|
+
License-Expression: 0BSD
|
|
9
|
+
Requires-Dist: capstone>=5.0.6
|
|
10
|
+
Requires-Dist: discord-py>=2.6.3
|
|
11
|
+
Requires-Dist: emoji>=2.14.1
|
|
12
|
+
Requires-Dist: googletrans>=4.0.2
|
|
13
|
+
Requires-Dist: jinja2>=3.1.6
|
|
14
|
+
Requires-Dist: loguru>=0.7.3
|
|
15
|
+
Requires-Dist: matplotlib>=3.10.6
|
|
16
|
+
Requires-Dist: msgpack>=1.1.1
|
|
17
|
+
Requires-Dist: pandas>=2.3.2
|
|
18
|
+
Requires-Dist: pefile>=2024.8.26
|
|
19
|
+
Requires-Dist: pillow>=11.3.0
|
|
20
|
+
Requires-Dist: pydantic>=2.11.9
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
22
|
+
Requires-Dist: requests>=2.32.5
|
|
23
|
+
Requires-Dist: requests-sse>=0.5.2
|
|
24
|
+
Requires-Dist: rich>=14.1.0
|
|
25
|
+
Requires-Dist: sqlitedict>=2.1.0
|
|
26
|
+
Requires-Dist: sqlmodel>=0.0.27
|
|
27
|
+
Requires-Dist: typer>=0.19.2
|
|
28
|
+
Requires-Dist: uwuipy>=0.1.10
|
|
29
|
+
Requires-Dist: yfinance>=0.2.66
|
|
30
|
+
Maintainer: Jay184
|
|
31
|
+
Maintainer-email: Jay184 <me@jay0.mozmail.com>
|
|
32
|
+
Requires-Python: >=3.11
|
|
33
|
+
Project-URL: Issues, https://github.com/Jay184/mikubot/issues
|
|
34
|
+
Project-URL: Repository, https://github.com/Jay184/mikubot.git
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# MikuBot
|
|
38
|
+
|
|
39
|
+
Bot used for the Eden Project discord server.
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
* Python 3.11 or above
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Windows
|
|
48
|
+
|
|
49
|
+
1. Create a `token.txt` which contains the bot's token.
|
|
50
|
+
1. Copy the `settings.json.template` and rename it to `settings.json` and fill it out with the necessary data.
|
|
51
|
+
1. run the `run.cmd`.
|
|
52
|
+
|
|
53
|
+
_Note: Make sure `py` starts a Python interpreter or change the `run.cmd` to use `python` instead._
|
|
54
|
+
|
|
55
|
+
## Acknowledgements
|
|
56
|
+
|
|
57
|
+
* [Bot Avatar](https://cdn.discordapp.com/app-icons/1078530934766321744/65ae221617e928f3ff928e535885d66d.png?size=256)
|
mikubot-0.1.0/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MikuBot
|
|
2
|
+
|
|
3
|
+
Bot used for the Eden Project discord server.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
* Python 3.11 or above
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Windows
|
|
12
|
+
|
|
13
|
+
1. Create a `token.txt` which contains the bot's token.
|
|
14
|
+
1. Copy the `settings.json.template` and rename it to `settings.json` and fill it out with the necessary data.
|
|
15
|
+
1. run the `run.cmd`.
|
|
16
|
+
|
|
17
|
+
_Note: Make sure `py` starts a Python interpreter or change the `run.cmd` to use `python` instead._
|
|
18
|
+
|
|
19
|
+
## Acknowledgements
|
|
20
|
+
|
|
21
|
+
* [Bot Avatar](https://cdn.discordapp.com/app-icons/1078530934766321744/65ae221617e928f3ff928e535885d66d.png?size=256)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mikubot"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Discord bot used for the Eden-Project Discord server."
|
|
5
|
+
keywords = ["discord", "eden", "miku", "vocaloid", "project diva"]
|
|
6
|
+
authors = [
|
|
7
|
+
{name = "Jay184", email = "me@jay0.mozmail.com"},
|
|
8
|
+
]
|
|
9
|
+
maintainers = [
|
|
10
|
+
{name = "Jay184", email = "me@jay0.mozmail.com"},
|
|
11
|
+
]
|
|
12
|
+
license = "0BSD"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"capstone>=5.0.6",
|
|
17
|
+
"discord-py>=2.6.3",
|
|
18
|
+
"emoji>=2.14.1",
|
|
19
|
+
"googletrans>=4.0.2",
|
|
20
|
+
"jinja2>=3.1.6",
|
|
21
|
+
"loguru>=0.7.3",
|
|
22
|
+
"matplotlib>=3.10.6",
|
|
23
|
+
"msgpack>=1.1.1",
|
|
24
|
+
"pandas>=2.3.2",
|
|
25
|
+
"pefile>=2024.8.26",
|
|
26
|
+
"pillow>=11.3.0",
|
|
27
|
+
"pydantic>=2.11.9",
|
|
28
|
+
"pydantic-settings>=2.10.1",
|
|
29
|
+
"requests>=2.32.5",
|
|
30
|
+
"requests-sse>=0.5.2",
|
|
31
|
+
"rich>=14.1.0",
|
|
32
|
+
"sqlitedict>=2.1.0",
|
|
33
|
+
"sqlmodel>=0.0.27",
|
|
34
|
+
"typer>=0.19.2",
|
|
35
|
+
"uwuipy>=0.1.10",
|
|
36
|
+
"yfinance>=0.2.66",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
mikubot = "mikubot:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Repository = "https://github.com/Jay184/mikubot.git"
|
|
44
|
+
Issues = "https://github.com/Jay184/mikubot/issues"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["uv_build>=0.8.12,<0.9.0"]
|
|
48
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .config import Settings
|
|
2
|
+
from .discord import Bot
|
|
3
|
+
from .logger import setup_logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
settings = Settings.load()
|
|
8
|
+
|
|
9
|
+
if settings.discord_key:
|
|
10
|
+
setup_logger(settings.logging)
|
|
11
|
+
client = Bot(settings)
|
|
12
|
+
client.run(settings.discord_key)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .sync import *
|
|
2
|
+
from .stop import *
|
|
3
|
+
from .reply_commands import *
|
|
4
|
+
from .settings import *
|
|
5
|
+
from .code import *
|
|
6
|
+
from .avatar import *
|
|
7
|
+
from .clap import *
|
|
8
|
+
from .edenpatch import *
|
|
9
|
+
from .gamebanana import *
|
|
10
|
+
from .it import *
|
|
11
|
+
from .list_brazil import *
|
|
12
|
+
from .purge import *
|
|
13
|
+
from .rename_channel import *
|
|
14
|
+
from .role import *
|
|
15
|
+
from .toggle_bot_response import *
|
|
16
|
+
from .user_count import *
|
|
17
|
+
from .uwu import *
|
|
18
|
+
from .you_are_going_to_brazil import *
|
|
19
|
+
from .zoe import *
|
|
20
|
+
from .guess import *
|
|
21
|
+
from .goofy_translate import *
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from discord import Interaction, Embed, Member
|
|
2
|
+
from discord.app_commands import describe, checks
|
|
3
|
+
from mikubot import Bot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register(bot: Bot):
|
|
7
|
+
@bot.tree.command(name="avatar", description="Get the avatar of a user.")
|
|
8
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
9
|
+
@describe(user="The user to retrieve the avatar of.")
|
|
10
|
+
async def handler(interaction: Interaction, user: Member):
|
|
11
|
+
embed = Embed(
|
|
12
|
+
title=f"{user.name}'s Avatar",
|
|
13
|
+
color=0x0099ff,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
embed.set_footer(text=bot.user.display_name)
|
|
17
|
+
embed.set_image(url=user.display_avatar.url)
|
|
18
|
+
|
|
19
|
+
await interaction.response.send_message( # noqa
|
|
20
|
+
embed=embed,
|
|
21
|
+
ephemeral=True,
|
|
22
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from discord import Interaction, Message
|
|
2
|
+
from discord.app_commands import describe, checks
|
|
3
|
+
from mikubot import Bot
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(bot: Bot):
|
|
8
|
+
@bot.tree.command(name="clap", description="Adds 👏 Clap 👏 Emoji 👏 in 👏 between 👏 each 👏 word.")
|
|
9
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
10
|
+
@describe(text="The text you want to add claps to.")
|
|
11
|
+
async def handler(interaction: Interaction, text: str):
|
|
12
|
+
text = re.sub(r"\s+", " 👏 ", text)
|
|
13
|
+
await interaction.response.send_message(text[:2000]) # noqa
|
|
14
|
+
|
|
15
|
+
@bot.tree.context_menu(name="clap")
|
|
16
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
17
|
+
async def context_handler(interaction: Interaction, message: Message):
|
|
18
|
+
text = re.sub(r"\s+", " 👏 ", message.content)
|
|
19
|
+
await interaction.response.send_message(text[:2000], allowed_mentions=None) # noqa
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from discord import Interaction
|
|
2
|
+
from discord.app_commands import describe, checks, choices, Choice
|
|
3
|
+
from capstone import Cs as Capstone, CS_ARCH_X86, CS_MODE_64, CS_OPT_SYNTAX_INTEL
|
|
4
|
+
from pefile import PE
|
|
5
|
+
from mikubot import Bot
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(bot: Bot):
|
|
9
|
+
def format_bytes(data: bytes, offset: int = 0) -> str:
|
|
10
|
+
lines = []
|
|
11
|
+
|
|
12
|
+
for i in range(0, len(data), 16):
|
|
13
|
+
chunk = data[i:i + 16]
|
|
14
|
+
hex_part = " ".join(f"{b:02X}" for b in chunk)
|
|
15
|
+
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
|
16
|
+
lines.append(f"{(offset + i):08X} {hex_part:<47} |{ascii_part}|")
|
|
17
|
+
|
|
18
|
+
return "\n".join(lines)
|
|
19
|
+
|
|
20
|
+
@bot.tree.command(name="code", description="Display binary code of MM+.")
|
|
21
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
22
|
+
@checks.has_role(1023036859883999272)
|
|
23
|
+
@describe(
|
|
24
|
+
version="The version of the game.",
|
|
25
|
+
address="The address to start reading.",
|
|
26
|
+
length="The length of the code to read.",
|
|
27
|
+
assembly="If the code should be displayed as assembly code.",
|
|
28
|
+
)
|
|
29
|
+
@choices(version=[
|
|
30
|
+
Choice(name="1.00", value=0),
|
|
31
|
+
Choice(name="1.01", value=1),
|
|
32
|
+
Choice(name="1.02", value=2),
|
|
33
|
+
Choice(name="1.03", value=3),
|
|
34
|
+
])
|
|
35
|
+
async def handler(interaction: Interaction, address: int, length: int = 0x20, assembly: bool = True, version: Choice[int] = 3):
|
|
36
|
+
md = Capstone(CS_ARCH_X86, CS_MODE_64)
|
|
37
|
+
md.syntax = CS_OPT_SYNTAX_INTEL
|
|
38
|
+
|
|
39
|
+
length = max(0x10, min(bot.settings.mm_code.max_length, length))
|
|
40
|
+
|
|
41
|
+
file_path = bot.settings.mm_code.binaries[version.value]
|
|
42
|
+
pe = PE(file_path, fast_load=True)
|
|
43
|
+
|
|
44
|
+
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
45
|
+
rva = address - image_base
|
|
46
|
+
data = pe.get_data(rva, length)
|
|
47
|
+
|
|
48
|
+
if assembly:
|
|
49
|
+
instructions = md.disasm(data, address, length)
|
|
50
|
+
message = "\n".join(f"{i.address:08X}: {i.mnemonic} {i.op_str}" for i in instructions)
|
|
51
|
+
await interaction.response.send_message(f"```x86asm\n{message}\n```") # noqa
|
|
52
|
+
else:
|
|
53
|
+
message = format_bytes(data, address)
|
|
54
|
+
await interaction.response.send_message(f"```properties\n{message}\n```") # noqa
|
|
55
|
+
|
|
56
|
+
pe.close()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from discord import Interaction, Embed
|
|
2
|
+
from discord.app_commands import checks
|
|
3
|
+
from mikubot import Bot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register(bot: Bot):
|
|
7
|
+
@bot.tree.command(name="edenpatch", description="Info on songlimit patch, song id patch, etc.")
|
|
8
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
9
|
+
async def handler(interaction: Interaction):
|
|
10
|
+
embed = Embed(
|
|
11
|
+
description=r"The Eden \_\_\_\_\_\_\_ Patch series of mods are public releases of mods that are included within Eden Project and as a result can cause some unintended side effects when enabled with Eden Project, as the mods are effectively running twice.",
|
|
12
|
+
color=0x86cecb,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
embed.set_author(name="Eden Patch", icon_url="https://images.gamebanana.com/img/ico/games/6296031c71087.png")
|
|
16
|
+
embed.set_footer(text=bot.user.display_name)
|
|
17
|
+
|
|
18
|
+
await interaction.response.send_message( # noqa
|
|
19
|
+
embed=embed,
|
|
20
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from discord import Interaction, Embed
|
|
2
|
+
from discord.app_commands import describe, checks
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from mikubot import Bot
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(bot: Bot):
|
|
10
|
+
def create_mod_embed(data: dict, full: bool = False) -> Embed:
|
|
11
|
+
embed = Embed(
|
|
12
|
+
title=data.get("_sName"),
|
|
13
|
+
url=data.get("_sProfileUrl"),
|
|
14
|
+
timestamp=datetime.fromtimestamp(data.get("_tsDateAdded"), timezone.utc),
|
|
15
|
+
color=0x86cecb,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Mark obselete
|
|
19
|
+
if data.get("_bIsObsolete", False):
|
|
20
|
+
embed.title = f"~~{embed.title}~~"
|
|
21
|
+
|
|
22
|
+
embed.set_author(
|
|
23
|
+
name=data.get("_aSubmitter", {}).get("_sName"),
|
|
24
|
+
icon_url=data.get("_aRootCategory", {}).get("_sIconUrl")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
first_image = data.get("_aPreviewMedia").get("_aImages")[0]
|
|
28
|
+
first_image_url = urljoin(first_image.get("_sBaseUrl", ""), first_image.get("_sFile", ""))
|
|
29
|
+
|
|
30
|
+
embed.set_thumbnail(url=first_image_url)
|
|
31
|
+
embed.set_footer(text=bot.user.display_name)
|
|
32
|
+
|
|
33
|
+
embed.add_field(
|
|
34
|
+
name="Submitter",
|
|
35
|
+
value=data.get("_aSubmitter", {}).get("_sName"),
|
|
36
|
+
inline=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
embed.add_field(
|
|
40
|
+
name="Likes",
|
|
41
|
+
value=data.get("_nLikeCount", 0),
|
|
42
|
+
inline=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
embed.add_field(
|
|
46
|
+
name="Views",
|
|
47
|
+
value=data.get("_nViewCount", 0),
|
|
48
|
+
inline=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if "_aAdditionalInfo" in data and "_sversion" in data.get("_aAdditionalInfo"):
|
|
52
|
+
embed.add_field(
|
|
53
|
+
name="Version",
|
|
54
|
+
value=data.get("_aAdditionalInfo", {}).get("_sversion", "Unknown version"),
|
|
55
|
+
inline=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# The following require a new API call
|
|
59
|
+
if full:
|
|
60
|
+
mod_id = data.get("_idRow")
|
|
61
|
+
profile_url = f"https://gamebanana.com/apiv10/Mod/{mod_id}/ProfilePage"
|
|
62
|
+
response = requests.get(profile_url)
|
|
63
|
+
|
|
64
|
+
if response.status_code == 200:
|
|
65
|
+
profile_data = response.json()
|
|
66
|
+
|
|
67
|
+
if "_aContentRatings" in profile_data and len(profile_data.get("_aContentRatings", {})):
|
|
68
|
+
embed.add_field(
|
|
69
|
+
name="Content Warnings",
|
|
70
|
+
value=", ".join(profile_data.get("_aContentRatings", {}).values()),
|
|
71
|
+
inline=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if "_sDescription" in profile_data:
|
|
75
|
+
embed.description = profile_data.get("_sDescription")
|
|
76
|
+
|
|
77
|
+
return embed
|
|
78
|
+
|
|
79
|
+
@bot.tree.command(name="gamebanana", description="Searches for a mod in the MegaMix+ section of Gamebanana.")
|
|
80
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
81
|
+
@describe(query="The mod name or submission ID to search for.")
|
|
82
|
+
async def handler(interaction: Interaction, query: str):
|
|
83
|
+
settings = bot.settings.gamebanana_search
|
|
84
|
+
|
|
85
|
+
await interaction.response.defer() # noqa
|
|
86
|
+
|
|
87
|
+
url = f"https://gamebanana.com/apiv10/Game/16522/Subfeed?_nPage=1&_nPerpage=10&_sSort=default&_sName={query}"
|
|
88
|
+
response = requests.get(url)
|
|
89
|
+
|
|
90
|
+
if response.status_code != 200:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
results = response.json().get("_aRecords") or []
|
|
94
|
+
|
|
95
|
+
# Limit results to N entries
|
|
96
|
+
result_count = min(len(results), settings.limit)
|
|
97
|
+
|
|
98
|
+
if result_count:
|
|
99
|
+
for i in range(0, result_count, 10):
|
|
100
|
+
embeds = [create_mod_embed(result, settings.full) for result in results[i:i+10]]
|
|
101
|
+
|
|
102
|
+
await interaction.followup.send( # noqa
|
|
103
|
+
f"Showing {result_count} our of {len(results)} results...",
|
|
104
|
+
embeds=embeds
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
embed = Embed(
|
|
108
|
+
title="No results found.",
|
|
109
|
+
color=0xffc526,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
embed.set_author(name="GameBanana Search", icon_url="https://images.gamebanana.com/static/img/mascots/detective.png")
|
|
113
|
+
embed.set_footer(text=bot.user.display_name)
|
|
114
|
+
|
|
115
|
+
await interaction.followup.send(embed=embed) # noqa
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from discord import Interaction, Message
|
|
2
|
+
from discord.app_commands import describe, checks
|
|
3
|
+
from googletrans import Translator
|
|
4
|
+
from googletrans.constants import LANGUAGES
|
|
5
|
+
|
|
6
|
+
from mikubot import Bot
|
|
7
|
+
import random
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def safe_translate(translator: Translator, text: str, src: str, dest: str, retries: int = 3) -> str:
|
|
12
|
+
"""Translate text with retries and delay on failure."""
|
|
13
|
+
for attempt in range(retries):
|
|
14
|
+
try:
|
|
15
|
+
translated = await translator.translate(text, src=src, dest=dest)
|
|
16
|
+
return translated.text
|
|
17
|
+
except Exception as e: # noqa
|
|
18
|
+
if attempt < retries - 1:
|
|
19
|
+
await asyncio.sleep(1 + attempt) # wait longer each retry
|
|
20
|
+
else:
|
|
21
|
+
# Give up, return what we have so far
|
|
22
|
+
return text
|
|
23
|
+
return text
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def mass_translate(text: str, hops: int = 10, initial_lang: str = "en", final_lang: str = "en") -> str:
|
|
27
|
+
current_text = text
|
|
28
|
+
hops = max(0, hops)
|
|
29
|
+
|
|
30
|
+
available_langs = list(LANGUAGES.keys())
|
|
31
|
+
|
|
32
|
+
if initial_lang in available_langs:
|
|
33
|
+
available_langs.remove(initial_lang)
|
|
34
|
+
|
|
35
|
+
if final_lang in available_langs:
|
|
36
|
+
available_langs.remove(final_lang)
|
|
37
|
+
|
|
38
|
+
async with Translator() as translator:
|
|
39
|
+
for _ in range(hops):
|
|
40
|
+
random_lang = random.choice(available_langs)
|
|
41
|
+
current_text = await safe_translate(translator, current_text, src=initial_lang, dest=random_lang)
|
|
42
|
+
initial_lang = random_lang
|
|
43
|
+
await asyncio.sleep(random.uniform(0.5, 1.5)) # small delay to reduce rate limit risk
|
|
44
|
+
|
|
45
|
+
current_text = await safe_translate(translator, current_text, src=initial_lang, dest=final_lang)
|
|
46
|
+
|
|
47
|
+
return current_text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register(bot: Bot):
|
|
51
|
+
@bot.tree.command(name="jumble", description="Google translates text through random languages before returning to the original language.")
|
|
52
|
+
@checks.cooldown(1, 10, key=lambda i: (i.guild_id,))
|
|
53
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
54
|
+
@describe(
|
|
55
|
+
text="The text to translate",
|
|
56
|
+
hops="How many languages to translate to",
|
|
57
|
+
initial_lang="Language to start with",
|
|
58
|
+
final_lang="Language to end on",
|
|
59
|
+
)
|
|
60
|
+
async def handler(interaction: Interaction, text: str, hops: int = 10, initial_lang: str = "en", final_lang: str = "en"):
|
|
61
|
+
await interaction.response.defer() # noqa
|
|
62
|
+
text = await mass_translate(text, hops, initial_lang, final_lang)
|
|
63
|
+
await interaction.followup.send(text[:2000]) # noqa
|
|
64
|
+
|
|
65
|
+
@bot.tree.context_menu(name="jumble")
|
|
66
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
67
|
+
async def context_handler(interaction: Interaction, message: Message):
|
|
68
|
+
# We could use discord.py's modals to query for hops and languages if we need to.
|
|
69
|
+
hops = 10
|
|
70
|
+
initial_lang = "en"
|
|
71
|
+
final_lang = "en"
|
|
72
|
+
|
|
73
|
+
await interaction.response.defer() # noqa
|
|
74
|
+
text = await mass_translate(message.content, hops, initial_lang, final_lang)
|
|
75
|
+
await interaction.followup.send(text[:2000], allowed_mentions=None) # noqa
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from discord import Interaction
|
|
4
|
+
from discord.app_commands import describe, checks
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from sqlitedict import SqliteDict
|
|
7
|
+
from mikubot import Bot
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Guess(BaseModel):
|
|
12
|
+
user_id: int
|
|
13
|
+
guess: list[str]
|
|
14
|
+
feedback: list[str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Puzzle(BaseModel):
|
|
18
|
+
solution: list[str]
|
|
19
|
+
pool: list[str]
|
|
20
|
+
guesses: list[Guess] = Field(default_factory=list)
|
|
21
|
+
solved_by: int | None = None
|
|
22
|
+
last_guess: datetime | None = None
|
|
23
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
24
|
+
is_public: bool = False
|
|
25
|
+
|
|
26
|
+
def is_solved(self) -> bool:
|
|
27
|
+
return self.solved_by is not None
|
|
28
|
+
|
|
29
|
+
def can_guess(self) -> bool:
|
|
30
|
+
return not self.is_solved() or self.is_public
|
|
31
|
+
|
|
32
|
+
def is_expired(self, max_age_minutes: int = 1440):
|
|
33
|
+
return (datetime.now(timezone.utc) - self.created_at).total_seconds() > (max_age_minutes * 60)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_sequence(pool: list[str], length: int, max_duplicates: int = 2) -> list[str]:
|
|
37
|
+
while True:
|
|
38
|
+
seq = [random.choice(pool) for _ in range(length)]
|
|
39
|
+
emoji_counts = {emoji: seq.count(emoji) for emoji in set(seq)}
|
|
40
|
+
if all(count <= max_duplicates for count in emoji_counts.values()):
|
|
41
|
+
if len(set(seq)) > 1: # Avoid boring ones
|
|
42
|
+
return seq
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def compare_guess(answer: list[str], guess: list[str]) -> list[str]:
|
|
46
|
+
feedback = [""] * len(guess)
|
|
47
|
+
answer_counter = Counter(answer)
|
|
48
|
+
|
|
49
|
+
# First pass: check for exact matches
|
|
50
|
+
for i in range(len(guess)):
|
|
51
|
+
if guess[i] == answer[i]:
|
|
52
|
+
feedback[i] = "✅"
|
|
53
|
+
answer_counter[guess[i]] -= 1 # Reserve matched emoji
|
|
54
|
+
|
|
55
|
+
# Second pass: check for partial matches
|
|
56
|
+
for i in range(len(guess)):
|
|
57
|
+
if feedback[i]: # Already matched
|
|
58
|
+
continue
|
|
59
|
+
if answer_counter[guess[i]] > 0:
|
|
60
|
+
feedback[i] = "⚠️"
|
|
61
|
+
answer_counter[guess[i]] -= 1
|
|
62
|
+
else:
|
|
63
|
+
feedback[i] = "❌"
|
|
64
|
+
|
|
65
|
+
return feedback
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def render_guess_history(puzzle: Puzzle) -> str:
|
|
69
|
+
if not puzzle.guesses:
|
|
70
|
+
return "_No guesses yet._"
|
|
71
|
+
|
|
72
|
+
lines = []
|
|
73
|
+
for guess in puzzle.guesses:
|
|
74
|
+
feedback = compare_guess(puzzle.solution, guess.guess)
|
|
75
|
+
guess_str = " ".join(guess.guess)
|
|
76
|
+
result_str = " ".join(feedback)
|
|
77
|
+
lines.append(f"- {guess_str} → {result_str}")
|
|
78
|
+
return "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def extract_emojis(text: str, emoji_pool: list[str]) -> list[str]:
|
|
82
|
+
result = []
|
|
83
|
+
i = 0
|
|
84
|
+
pool_sorted = sorted(emoji_pool, key=len, reverse=True) # match longer first
|
|
85
|
+
|
|
86
|
+
while i < len(text):
|
|
87
|
+
match = None
|
|
88
|
+
for emoji in pool_sorted:
|
|
89
|
+
if text.startswith(emoji, i):
|
|
90
|
+
match = emoji
|
|
91
|
+
break
|
|
92
|
+
if match:
|
|
93
|
+
result.append(match)
|
|
94
|
+
i += len(match)
|
|
95
|
+
else:
|
|
96
|
+
i += 1 # skip non-matching character
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def register(bot: Bot):
|
|
102
|
+
def create_puzzle(user_id: int) -> Puzzle:
|
|
103
|
+
sequence = generate_sequence(
|
|
104
|
+
bot.settings.guessing_game.emoji_pool,
|
|
105
|
+
bot.settings.guessing_game.sequence_length,
|
|
106
|
+
bot.settings.guessing_game.max_duplicates,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
puzzle = Puzzle(solution=sequence, pool=bot.settings.guessing_game.emoji_pool.copy())
|
|
110
|
+
|
|
111
|
+
with SqliteDict(
|
|
112
|
+
bot.settings.storage_file,
|
|
113
|
+
tablename="guessgame",
|
|
114
|
+
outer_stack=False,
|
|
115
|
+
) as db:
|
|
116
|
+
db[str(user_id)] = puzzle.model_dump()
|
|
117
|
+
db.commit()
|
|
118
|
+
|
|
119
|
+
return puzzle
|
|
120
|
+
|
|
121
|
+
def get_puzzle(user_id: int) -> Puzzle:
|
|
122
|
+
with SqliteDict(
|
|
123
|
+
bot.settings.storage_file,
|
|
124
|
+
tablename="guessgame",
|
|
125
|
+
outer_stack=False,
|
|
126
|
+
) as db:
|
|
127
|
+
puzzle_data = db.get(str(user_id))
|
|
128
|
+
|
|
129
|
+
if puzzle_data:
|
|
130
|
+
puzzle = Puzzle.model_validate(puzzle_data)
|
|
131
|
+
|
|
132
|
+
if puzzle.is_solved() or puzzle.is_expired():
|
|
133
|
+
return create_puzzle(user_id)
|
|
134
|
+
|
|
135
|
+
return puzzle
|
|
136
|
+
else:
|
|
137
|
+
return create_puzzle(user_id)
|
|
138
|
+
|
|
139
|
+
@bot.tree.command(name="codebreaker", description="Start a codebreaker game or continue your current one with a guess.")
|
|
140
|
+
@checks.bot_has_permissions(send_messages=True)
|
|
141
|
+
@describe(guess="Your guess. Leave empty to see current state or generate a new puzzle.")
|
|
142
|
+
async def handler(interaction: Interaction, guess: str = ""):
|
|
143
|
+
puzzle = get_puzzle(interaction.user.id)
|
|
144
|
+
|
|
145
|
+
if not guess:
|
|
146
|
+
history = render_guess_history(puzzle)
|
|
147
|
+
emoji_list = " ".join(puzzle.pool)
|
|
148
|
+
|
|
149
|
+
await interaction.response.send_message( # noqa
|
|
150
|
+
f"Here is your puzzle:\n\n"
|
|
151
|
+
f"Guess the sequence of {len(puzzle.solution)} emojis!\n"
|
|
152
|
+
f"Use _/codebreaker (your guess)_ to solve.\n"
|
|
153
|
+
f"Available emojis: {emoji_list}\n\n"
|
|
154
|
+
f"**Your guesses so far:**\n{history}",
|
|
155
|
+
ephemeral=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
guess_emojis = extract_emojis(guess, puzzle.pool)
|
|
161
|
+
|
|
162
|
+
if len(guess_emojis) != len(puzzle.solution):
|
|
163
|
+
await interaction.response.send_message( # noqa
|
|
164
|
+
f"You must enter exactly {len(puzzle.solution)} emojis from the allowed pool: {" ".join(puzzle.pool)}",
|
|
165
|
+
ephemeral=True
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
feedback = compare_guess(puzzle.solution, guess_emojis)
|
|
170
|
+
is_correct = all(symbol == "✅" for symbol in feedback)
|
|
171
|
+
|
|
172
|
+
puzzle.guesses.append(Guess(user_id=interaction.user.id, guess=guess_emojis, feedback=feedback))
|
|
173
|
+
puzzle.last_guess = datetime.now(timezone.utc)
|
|
174
|
+
|
|
175
|
+
with SqliteDict(
|
|
176
|
+
bot.settings.storage_file,
|
|
177
|
+
tablename="guessgame",
|
|
178
|
+
outer_stack=False,
|
|
179
|
+
) as db:
|
|
180
|
+
if is_correct:
|
|
181
|
+
puzzle.solved_by = interaction.user.id
|
|
182
|
+
|
|
183
|
+
await interaction.response.send_message( # noqa
|
|
184
|
+
f"Correct!\n\n"
|
|
185
|
+
f"Solution: {" ".join(guess_emojis)} → {" ".join(feedback)}\n\n"
|
|
186
|
+
f"**Your guess history:**\n{render_guess_history(puzzle)}",
|
|
187
|
+
ephemeral=True
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
del db[str(interaction.user.id)]
|
|
191
|
+
db[f"{interaction.user.id}:{hash(puzzle.created_at)}"] = puzzle.model_dump()
|
|
192
|
+
else:
|
|
193
|
+
await interaction.response.send_message( # noqa
|
|
194
|
+
f"Not quite!\n"
|
|
195
|
+
f"Your guess: {" ".join(guess_emojis)} → {" ".join(feedback)}\n\n"
|
|
196
|
+
f"**Your guess history:**\n{render_guess_history(puzzle)}",
|
|
197
|
+
ephemeral=True
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
db[str(interaction.user.id)] = puzzle.model_dump()
|
|
201
|
+
|
|
202
|
+
db.commit()
|