218 Commits

Author SHA1 Message Date
neoarz
e086dd4351 chore: bleh 2025-12-29 19:40:52 -05:00
neoarz
63e26bac74 fix: sidestore message and preview 2025-12-29 18:17:10 -05:00
neo
bc2cfb57d1 feat: sticky message (#29) 2025-12-29 17:56:33 -05:00
neoarz
6d61482216 chore: rm download 2025-12-10 13:19:43 -05:00
CelloSerenity
b7d010e44c feat: Update mountddi, replace the pairing commands, switch to localdevvpn
* Update mountddi.py

* Update pairing command description and embed content

* Update crash.py

* Update afc.py

* Update VPN app reference in error message

* Update footer text in embed message

* feat: update mount ddi to be downloaded on the fly

also fix some small errors and update errorcodes.json

---------

Co-authored-by: neoarz <email@neoarz.dev>
2025-12-07 18:08:09 -05:00
CelloSerenity
0d3fcb8146 fix: Update to iloader and fix broken links (#26)
* Update afc.py

* Update afc.py

* Update error message and documentation link for UDID

* Update pairing file instructions and documentation URL

* Update pairing file instructions in refresh.py

* Update sparse.py

* NOTE TO SELF: This can probably be removed altogether or modified drastically because we don’t use jitterbug anymore

* Update jit26.py
2025-11-27 14:14:49 -05:00
neoarz
23790c46b4 chore: ruff fix 2025-11-22 19:51:46 -05:00
neo
4a9b6b1f06 feat: livecontainer command group (#25)
* initial commit

* fix: image link

* feat: finish cellos command

* fix: interactive sounds wierd 😭
2025-11-22 14:43:31 -05:00
Daisuke
bafd8fc19a fix: idevice errorcodes bug (#24)
* very easy fix

* Added self hosting instructions

* nvm this was the actual fix  nooo way

* removed instructions :3

* fix: some stuff

---------

Co-authored-by: neoarz <email@neoarz.dev>
2025-11-15 20:46:39 -05:00
neoarz
11f08556dc chore: discord.py ver latest 2025-11-14 11:04:16 -05:00
neoarz
16f2396507 chore: remove embed links, only keep content 2025-11-08 17:43:27 -05:00
neoarz
783333cc53 feat(melonx): upgrade.py new command 2025-11-03 15:47:23 -05:00
neoarz
44c3c454aa fix(mention): 1 line breh 2025-11-03 15:34:05 -05:00
neoarz
480b5ce83a refactor(mention): move from bot.py to event cog 2025-11-03 15:15:52 -05:00
neoarz
1eff6c9f53 chore: ruff formatting 2025-11-02 23:32:52 -05:00
neo
2ce2c69a87 feat: baitbot 2025-11-02 18:57:40 -05:00
Meshal Alharbi
ee2a780a96 chore(melonx): update version 2025-11-02 18:54:36 -05:00
neo
223e2ebdc7 chore: add halloween icon 2025-10-31 11:39:11 -04:00
neoarz
1da81e68d6 chore: ping command not empheral 2025-10-25 15:32:22 -04:00
neoarz
4422bd1689 fix(logs): now are empheral 2025-10-22 09:06:00 -04:00
neoarz
05b26b55a4 chore: move checks before command is procesed 2025-10-20 12:30:55 -04:00
neoarz
a5f94df549 chore: add permission checks 2025-10-20 12:21:43 -04:00
neoarz
898e01abf4 chore: server bot not selfbot self, selfuse slowing being rolled out 2025-10-20 11:57:49 -04:00
Meshal Alharbi
6d596713fb chore: last edited 2025-10-19 13:12:21 -04:00
Stossy11
5fb7c1b48b feat(legal): new command :)
Co-authored-by: Stossy11 <stossy11@stossy11.com>
Co-authored-by: neoarz <tyrantneo740@gmail.com>
2025-10-19 11:36:02 -04:00
neoarz
4b651420a0 fix(color): add embed icon 2025-10-18 10:50:32 -04:00
Meshal Alharbi
1d681d4fe0 feat(color): new command :)
Co-authored-by: neo <email@neoarz.dev>
Co-authored-by: neoarz <tyrantneo740@gmail.com>
2025-10-18 10:00:34 -04:00
Dochbert
6d164033e1 fix(readme): add missing command 2025-10-18 09:24:53 -04:00
neoarz
cc01c60250 refactor(codepreview): no dms 2025-10-18 00:19:04 -04:00
neoarz
c286c2a070 refactory(say): now works in dms and priv channels 2025-10-17 23:35:29 -04:00
neoarz
025df159f7 refactor(img2gif): now works in dms and priv channels 2025-10-17 23:13:35 -04:00
neoarz
352067d51d refactor(tts): now works in dms and priv channels 2025-10-17 23:05:41 -04:00
neoarz
f5227e5f0f refactor(mcquote): now works in dms 2025-10-17 23:03:25 -04:00
neoarz
e271333de1 fix(download): now works in dms and priv channels 2025-10-17 20:49:56 -04:00
Meshal Alharbi
92905435a0 feat(melonx): add discord buttons for melonx commands
Co-authored-by: neoarz <tyrantneo740@gmail.com>
2025-10-17 11:23:37 -04:00
neoarz
e046f3292a chore(todo): new todo 2025-10-17 02:09:58 -04:00
neoarz
c8e69b230d chore(todo): new todo 2025-10-17 02:07:13 -04:00
neoarz
b18355e749 feat(botinfo): contributors!!! 2025-10-17 01:44:26 -04:00
neoarz
61848f6bc8 feat(silly): new commad & botinfo now does discord-py version 2025-10-17 00:15:06 -04:00
neoarz
7955035b3b chore(botinfo): mention /help to get started 2025-10-16 17:52:05 -04:00
neoarz
db0368eb4d feat(duck): new command & update todo 2025-10-16 01:02:56 -04:00
neoarz
c9626e779f chore(license): syntrel is now MIT 2025-10-16 00:25:30 -04:00
neoarz
cb1596a1d8 feat(bot): make syntrel work in dms and group chats 2025-10-15 15:58:57 -04:00
se2crid
6a43c93637 feat(tts): new command
Co-authored-by: neoarz <tyrantneo740@gmail.com>
2025-10-15 00:35:19 -04:00
neoarz
14cb324e0e fix(say): empheral message auto deletes 2025-10-14 17:31:57 -04:00
neoarz
9e135d29d8 feat(owner): co owners yayy 2025-10-14 17:24:41 -04:00
yellowstar520
372e1e45a3 feat(sigma): new command
Co-authored-by: neoarz <tyrantneo740@gmail.com>
2025-10-14 16:18:05 -04:00
CelloSerenity
5c6f12ab80 fix(sidestore): update idevice_pair terminology 2025-10-14 15:08:33 -04:00
neoarz
6c6555dfb7 fix(minesweeper): no bomb on first press 2025-10-11 11:55:53 -04:00
neo
356d47f812 Merge pull request #10 from neoarz/userinfo 2025-10-11 00:41:13 -04:00
neoarz
2dab7ade22 fix(userinfo): remove badge 2025-10-11 00:39:47 -04:00
neoarz
d381c5cbf0 fix(userinfo): icons 2025-10-10 23:46:23 -04:00
neoarz
7a4e4ed0f8 fix(userinfo): remove unused functions 2025-10-10 22:32:15 -04:00
neoarz
1e36534b41 feat(userinfo): custom badges for owners yay 2025-10-10 22:28:43 -04:00
neoarz
b1a5267f02 fix(userinfo): fallback for no guild 2025-10-10 15:56:19 -04:00
neoarz
89f5aecfd5 fix(userinfo): banner sizes 2025-10-10 15:49:44 -04:00
neoarz
8f962b2fde refactory(userinfo): embed color 2025-10-10 15:31:19 -04:00
neoarz
a94a99ac29 feat(bot): more intents 2025-10-10 13:33:18 -04:00
neoarz
0693950468 feat(userinfo): initial commit 2025-10-10 12:21:34 -04:00
neoarz
ed53eb375d fix(minesweeper): dont time out game after user has won/lost 2025-10-10 10:20:38 -04:00
neoarz
a436388db4 fix(botinfo): emoji rip 2025-10-10 00:40:50 -04:00
neoarz
b06b0cdb66 refactor(serverinfo): new info
Enhanced the server info embed with more detailed statistics and improved formatting. Updated help command to send responses in the channel when possible, with fallback to DMs if sending fails, and added error handling for users with DMs disabled. Made botinfo embed non-ephemeral and added a custom emoji to the description.
2025-10-09 22:56:43 -04:00
neo
0fe0f4550a Merge pull request #8 from neoarz/readme 2025-10-09 21:50:48 -04:00
neoarz
2ff4513ebf fix(botinfo): wording 2025-10-09 21:50:15 -04:00
neo
b374223b6c fix(readme):formatting of command group table in README 2025-10-09 16:28:06 -04:00
neo
c2b0f318c1 refactor(readme): badge for total commands
Added a badge for total commands in the README.
2025-10-09 10:29:03 -04:00
neoarz
92e0ab72de fix(readme): sigh 2025-10-09 08:20:52 -04:00
neoarz
c988bac1b0 feat(readme): stuff 2025-10-09 08:18:43 -04:00
neoarz
24d0a99055 @feat(botinfo): update message, and include message on new server join 2025-10-09 08:02:52 -04:00
neoarz
d2fbb5f55b refactor(botinfo): new ui 2025-10-09 01:00:00 -04:00
neoarz
50af638e2d refactor(botinfo): now seperate command 2025-10-09 00:24:43 -04:00
neoarz
71cb384ff3 feat(codepreview): diff colors for pr status 2025-10-08 23:40:25 -04:00
neoarz
743bf82bda feat(codepreview): now supports prs!!! 2025-10-08 23:28:30 -04:00
neoarz
4bb78d6eae fix(tweety): new api with spam protection 2025-10-08 21:55:13 -04:00
neoarz
dccf979a5a fix(docs): stuff 2025-10-08 20:46:55 -04:00
neoarz
beffefcb14 fix(minesweeper: no more ratelimiting? 2025-10-08 16:03:54 -04:00
neoarz
94a0e89dc3 feat(docs): new command :) 2025-10-08 15:29:01 -04:00
neoarz
087b8095e2 refactor(misc): send images directly instead of embed 2025-10-08 09:49:34 -04:00
neo
c099df1a45 Merge pull request #7 from neoarz/tweety 2025-10-08 00:44:28 -04:00
neoarz
c80dca059f fix(tweety): warning and word count 2025-10-08 00:43:21 -04:00
neoarz
f946efd5ce chore: update readme and todo 2025-10-07 23:00:33 -04:00
neoarz
78f5e80b93 fix(tweet): dual error messages 2025-10-07 22:52:06 -04:00
neoarz
fae21eed44 fix(tweety): only works with text no more images
also now works with other bot messages
2025-10-07 22:41:05 -04:00
neoarz
94d0eca8f1 fix(tweet): user and role mentions now show real text 2025-10-07 22:24:30 -04:00
neoarz
47043fb38b refactor(tweet): new york time
add pytz requirements
2025-10-07 22:10:52 -04:00
neoarz
56818bf912 refactor(tweety): new slash command for tweety (shows help) 2025-10-07 22:02:37 -04:00
neoarz
eb800a9bd5 refactor(tweety): too much unecessary code :) 2025-10-07 21:37:55 -04:00
neoarz
7419ec5f59 fix(tweety): remove theme args since buttons work
Simplifies the tweety command by removing the 'verified' and 'theme' arguments from command signatures and related logic. Defaults for these options are now set internally, and toggling is handled via UI elements instead of command parameters.
2025-10-07 21:29:42 -04:00
neoarz
70af8e24b2 fix(tweety): slash commands show diff message 2025-10-07 15:22:33 -04:00
neoarz
25ea98236d feat(tweety): new command :) 2025-10-07 13:33:53 -04:00
neoarz
9c7359906c feat(depart): new command :) (requested by jackson) 2025-10-06 21:25:14 -04:00
neo
78f6959d04 Merge pull request #6 from CelloSerenity/Csdev 2025-10-06 12:40:56 -04:00
neoarz
18bac4a125 fix(crash): use double quotes to not break file
- update files last updated
- remove original names since using /(command group) help is better
2025-10-06 08:38:39 -04:00
CelloSerenity
7dc62b9340 DOWN 2025-10-05 22:17:51 -06:00
CelloSerenity
9943e6fcc3 YOU 2025-10-05 22:14:56 -06:00
CelloSerenity
2f2a426fe0 LET 2025-10-05 21:45:16 -06:00
CelloSerenity
32b5f1cc32 GONNA 2025-10-05 21:42:02 -06:00
CelloSerenity
abebd06227 NEVER 2025-10-05 21:41:04 -06:00
CelloSerenity
b5c3f0f93e UP 2025-10-05 21:38:45 -06:00
CelloSerenity
c07743dc75 YOU 2025-10-05 21:38:16 -06:00
CelloSerenity
19c20af504 GIVE
note: maybe change name to anisette?
2025-10-05 21:37:22 -06:00
CelloSerenity
1b636e6621 GONNA 2025-10-05 21:35:43 -06:00
CelloSerenity
f9207c3f56 NEVER 2025-10-05 21:34:33 -06:00
neoarz
cc3e5e4a75 fix(translate): make markdown render properly 2025-10-05 20:03:25 -04:00
neoarz
11b9effcb6 feat(timeout): new command :) 2025-10-05 14:38:30 -04:00
neoarz
f7f4372cf7 feat(img2gif): new command :) 2025-10-05 09:07:10 -04:00
neoarz
f26da6bf80 fix(hackban): was looking for str instead of int rip
Enhanced the feedback command's error response to use an embed for better user experience. Changed the 'user_id' parameter type from int to str in hackban commands to support non-integer user IDs.
2025-10-04 22:09:11 -04:00
neoarz
6fc24ef8f6 fix(feedback): wasnt working due to app commands 2025-10-04 20:21:10 -04:00
neoarz
5128706f9a fix(tryitandsee): send only link instead of embed
The tryitandsee command now sends a direct link (https://tryitands.ee/) instead of an embedded GIF. This simplifies the response and removes the use of embeds and images.
2025-10-04 19:31:06 -04:00
neoarz
4f66c3705a fix(8ball && mcquote): fixes
Replaces the synchronous requests library with aiohttp in mcquote.py to enable asynchronous HTTP requests, improving performance and compatibility with async Discord bots. Also updates exception handling to use aiohttp.ClientError. Adds missing discord import in eightball.py.
2025-10-04 18:43:06 -04:00
neoarz
b26d786d23 feat(purge): make it able to purge specific user messages 2025-10-04 13:22:54 -04:00
neoarz
592d697140 feat(mcquote): new command :)
Introduces a new /mcquote command that generates a custom Minecraft achievement quote image using skinmc.net. Updates README and media cog to register and document the new command.
2025-10-04 12:29:01 -04:00
neoarz
804eaa39a6 fix(melonx/ios26): update channel link
Added MeloNX Discord link to README, updated TODO.md to mark melonx commands as completed, and updated the iOS 26 command with the correct Discord channel link for MeloNX.
2025-10-04 09:26:20 -04:00
neoarz
ad1e117e75 feat(melonx): new melonx commands
Introduced new commands to the MeloNX cog: mods, gamecrash, requirements, error, and 26 (iOS 26), each with their own handler and help embed. Updated the README and melonx.py select menu to reflect the new commands. These additions provide users with troubleshooting and informational commands for common MeloNX issues and topics.
2025-10-04 08:54:48 -04:00
neoarz
023ec85640 fix(download): fix 403 error discord 2025-10-03 22:18:09 -04:00
neoarz
b28b4e2cbc chore(todo):new todo 2025-10-03 21:27:12 -04:00
neoarz
ec9c90e4e8 feat(melonx): cog with transfer command and help integration
Introduces a new 'melonx' cog with a dropdown help menu and a 'transfer' command for save file migration instructions. Updates help command categories and README to include MeloNX. Implements UI components for command selection and embeds for user guidance.
2025-10-03 21:12:44 -04:00
neoarz
73535e283d refactor(invite): command message handling
Refactored the invite command to always send the invite embed in the current context instead of attempting to DM the user first. This removes the try/except logic and ensures consistent behavior.
2025-10-03 18:30:27 -04:00
neoarz
9fafc3ca4f fix(download): local development cookie path 2025-10-03 15:49:33 -04:00
neoarz
55286a8d15 fix(download): cookies path and download 2025-10-03 15:38:48 -04:00
neoarz
4cc42ba7da fix(dockerfile): add ffmpeg install for stuff 2025-10-03 13:09:47 -04:00
neoarz
2c0b0afb39 fix(download): support for yt-dlp cookies in media download
Introduces environment variable support for specifying yt-dlp cookies via file path or direct content for the media download command. Updates error handling to provide clearer messages when cookies are required and ensures temporary cookie files are cleaned up. Also updates .env.example and .gitignore to reflect these changes.
2025-10-03 12:49:50 -04:00
neoarz
85a43e66b0 feat(media): media download command using yt-dlp
Introduced a new 'media' command group with a 'download' subcommand for downloading videos via yt-dlp. Updated help and README to include the new media category and command. Added yt-dlp to requirements.txt.
2025-10-03 12:10:45 -04:00
neoarz
f4c3b4aae7 refactor(general): new author image
Replaced the embed author icon URLs from 'y5SEZ9.webp' to 'gSxqzV.png' across botinfo, feedback, ping, serverinfo, and uptime cogs for consistency and to use the new image.
2025-10-03 11:27:19 -04:00
neoarz
4e258dd174 fix(dictionary): make examples in new lines and add divider 2025-10-02 21:21:39 -04:00
neoarz
b0a82ed07e feat(dictonary):new command 2025-10-02 21:07:45 -04:00
neoarz
b770ff8f37 fix(translate):auto-complete not showing 2025-10-02 19:21:59 -04:00
neoarz
3f0cd619ac fix(help): only show subcommands 2025-10-01 14:16:26 -04:00
neoarz
60d54c434b feat(codepreview): new command 2025-10-01 14:16:26 -04:00
neoarz
c0afe06e4d feat(support): new support command 2025-09-29 22:11:14 -04:00
neoarz
6ad1e296f1 feat(dontasktoask): new command 2025-09-29 21:17:04 -04:00
neoarz
e92b768294 fix(ascii): color of logo 2025-09-29 20:57:43 -04:00
neo
0cd8f3fbe2 refactor(everything): combined all commands into command groups 2025-09-29 10:22:46 -04:00
neoarz
3f4254dcdc feat: Require group prefix for hybrid commands
Added a _require_group_prefix check to all hybrid commands in each cog to ensure commands are only executed when invoked with the appropriate group prefix. Also updated error handling in bot.py to silently ignore CheckFailure errors. This improves command organization and prevents accidental command execution outside their intended context.
2025-09-29 09:59:56 -04:00
neoarz
f8123b43a2 feat: Add aliases and update group names for cogs
Renamed 'miscellaneous' and 'utilities' cog group names to 'misc' and 'utils', respectively. Added aliases for command groups and updated help descriptions and embed author icons for consistency and clarity.
2025-09-29 09:19:00 -04:00
neoarz
a67e80a6eb refactor: group commands for miscellaneous and utilities cogs
Introduces group commands for both miscellaneous and utilities cogs, allowing users to access subcommands via `.miscellaneous <subcommand>` and `.utilities <subcommand>`. Adds embedded help messages and hybrid invocation logic for easier command management and user guidance.
2025-09-29 08:46:21 -04:00
neoarz
2973d0fd25 refactor(general): command groups
Introduces a 'general' command group with subcommands for ping, uptime, botinfo, serverinfo, and feedback. Provides an embed listing available subcommands and routes each to their respective command handlers for improved organization and discoverability.
2025-09-29 08:33:01 -04:00
neoarz
49fcd821a4 refactor(fun): command group with subcommands
Introduces a 'fun' command group with subcommands for coinflip, 8ball, minesweeper, randomfact, and rps. Provides an embed listing available subcommands and routes each to its respective handler for improved command organization.
2025-09-29 08:27:59 -04:00
neoarz
aea98a95d5 refactor(idevice): cog commands and add help group
Reworked the idevice cog to use a command group with subcommands for help, errorcodes, developermode, noapps, and mountddi. Added a dropdown view for help commands and improved logging for extension loading. Updated references to ideviceView and adjusted extension load conditions in bot.py.
2025-09-29 08:24:14 -04:00
neoarz
a7cdeee470 refactor(SideStore): help command structure
Reorganized SideStore commands to use a group structure, added individual subcommands for troubleshooting topics, and updated the help command to provide a dropdown for specific issues. This improves command discoverability and user experience.
2025-09-29 08:14:33 -04:00
neoarz
b6c74828f1 refactor: extension loading and help category mapping
Simplifies extension loading log conditions to only exclude 'owner' and streamlines help command category mapping by grouping commands under their respective __init__.py structures. Also updates subcommand handling to exclude only 'owner' from group checks.
2025-09-28 23:41:42 -04:00
neoarz
a5eff449eb refactor(utility): make command group
Introduces a new 'utilities' cog and a 'translate' command for translating text between languages. Refactors the translate functionality from a class-based cog to a function-based command, updates bot and help modules to register the new cog, and ensures proper language autocomplete and error handling.
2025-09-28 23:35:39 -04:00
neoarz
3bc5ebe192 refactor(sidestore): commands into single GroupCog
Converted individual sidestore command cogs into functions and grouped them under a new Sidestore GroupCog in cogs/sidestore/__init__.py. Updated bot.py and help.py to recognize the new 'sidestore' category. This simplifies command registration and improves maintainability.
2025-09-28 23:28:53 -04:00
neoarz
5e2999e565 refactor(miscellaneous): cogs to command functions
Converted individual miscellaneous cogs (keanu, labubu, piracy, rickroll, tryitandsee) from class-based to function-based command definitions. Added a new __init__.py to group these commands under a single Miscellaneous GroupCog for easier management and setup.
2025-09-28 23:08:07 -04:00
neoarz
e5ea7f1dac refactor(moderation): commands into group cog
Converted individual moderation command cogs (ban, kick, purge, warnings, archive, hackban, nick) into command factory functions and registered them under a new Moderation GroupCog in cogs/moderation/__init__.py. Updated bot.py and help.py to support the new moderation group and category. This improves organization and enables grouped moderation commands.
2025-09-28 23:07:46 -04:00
neoarz
51393ece85 refactor(idevice): commands into a single GroupCog
Merged idevice-related commands into a unified GroupCog in cogs/idevice/__init__.py, replacing individual Cog classes with command factory functions. Updated bot.py and help.py to support the new structure and improved command categorization. This refactor simplifies extension loading and command management for idevice troubleshooting features.
2025-09-28 22:57:26 -04:00
neoarz
72cdd9b403 refactor(general): commands into GroupCog
Migrated general commands (ping, uptime, botinfo, serverinfo, feedback) into a single GroupCog in cogs/general/__init__.py for better organization and maintainability. Converted individual command files to export command functions instead of Cogs. Updated bot.py to load the new general extension. Renamed help.py for consistency.
2025-09-28 22:53:25 -04:00
neoarz
e7ee97074b refactor(fun): cogs into one group
Combined all fun commands into a single 'fun' GroupCog with subcommands, replacing individual cogs for coinflip, eightball, minesweeper, randomfact, and rockpaperscissors. Updated bot.py to load the fun cog as a package and adjusted help.py to reflect the new command structure. This improves organization and discoverability of fun commands.
2025-09-28 22:48:10 -04:00
neoarz
6ed3b0d378 chore(todo): finished translate command 2025-09-28 21:42:52 -04:00
neoarz
4948d2edf7 refactor(bot): bot utilities into separate modules
Moved logging, signal handling, and uptime calculation logic from bot.py into dedicated utils modules for better organization and reusability. Updated imports and usage in bot.py and utils/__init__.py accordingly.
2025-09-28 16:20:28 -04:00
neoarz
0a262c522e fix(translate): fix embedding of translate command and remove TranslateView
The translate command now accepts text from replied messages if no text is provided, improving usability. The TranslateView and related UI logic have been removed, simplifying the translation output to a single embed message. Error handling and embed formatting have also been updated for consistency.
2025-09-28 16:01:54 -04:00
neoarz
bcb8cf3326 feat(read description): Add translate utility cog and update categories
Introduces a new 'translate' command under a utilities category, with language autocomplete and Google Translate integration. Updates README and help command to reflect the new category and command. Renames 'rr.py' to 'rickroll.py'. Updates moderation cogs to use a new icon URL for embed authors.
2025-09-28 13:19:28 -04:00
neoarz
85526653f2 feat(rr): make new rickroll cmd 2025-09-28 11:02:19 -04:00
neoarz
c236ebd84a feat(tryitandsee): new command 2025-09-28 10:53:12 -04:00
neoarz
00a49fa040 chore(todo): add new things :) 2025-09-27 19:52:10 -04:00
neo
d790dc64ef chore(todo): update tasks
Added a new task to include git log in info section.
2025-09-27 11:26:12 -04:00
neoarz
a0e8665f6d chore(todo): update list 2025-09-27 09:13:37 -04:00
neoarz
474cb244a9 feat(logs): add new owner only logs command 2025-09-27 09:07:42 -04:00
neoarz
c2c12d38ce fix(refresh): shorten command && update links 2025-09-27 08:34:17 -04:00
neoarz
158d1903a9 feat(ascii): new ascii art for term and fix piracy embed color 2025-09-27 08:23:56 -04:00
neoarz
12349e39f6 chore(readme): add new commands to list 2025-09-27 07:10:07 -04:00
neoarz
67aed99e5a fix(udid): fix command to have button to documentation 2025-09-27 07:06:11 -04:00
neoarz
7f3efa964c feat(piracy): add piracy command 2025-09-26 23:41:55 -04:00
neoarz
8d5db7f4cc fix(mountddi): fix so that it doesnt timeout 2025-09-26 21:05:13 -04:00
neoarz
f9923e0479 feat(files): mkdir for idevice files 2025-09-26 20:49:10 -04:00
neoarz
a305c87f4a feat(idevice): add developermode command 2025-09-26 20:30:46 -04:00
neoarz
bb46f61a61 fix(afc): idevice_pair naming 2025-09-26 20:03:45 -04:00
neo
5907487d31 Merge pull request #4 from CelloSerenity/CSdev 2025-09-25 13:08:03 -04:00
CelloSerenity
fd2f83765f Correct status names for consistency 2025-09-24 17:26:03 -06:00
neoarz
ad1ae76e6c chore(assets): add icon and discord banner 2025-09-24 12:50:23 -04:00
neoarz
e0640227b9 fix(help): errorcodes not showing 2025-09-24 11:04:07 -04:00
neo
5509f19647 chore(readme): guess original is best.. 2025-09-24 10:55:31 -04:00
neo
3b5a2793da chore(readme): command amount 2025-09-24 10:12:38 -04:00
neoarz
c980b36915 chore(readme): im picky asf 2025-09-24 10:08:31 -04:00
neo
8f1f0e0eeb chore(readme): remove useless note
Removed note about bot customization and usage.
2025-09-24 09:53:15 -04:00
neoarz
5398d16528 chore(idevice): better wording 2025-09-24 09:38:32 -04:00
neo
6303234e65 chore(readme): remove download
Clarified the note about downloading the bot.
2025-09-24 08:27:03 -04:00
neoarz
b492d60db3 fix(idevice): fix error commands 2025-09-24 08:24:42 -04:00
neoarz
48d1b875a9 fix(docker): show logs 2025-09-24 07:23:56 -04:00
neoarz
b4df04acc5 fix(error_codes): make command hybrid 2025-09-24 00:00:48 -04:00
neoarz
cd810659b4 fix(readme): wrong formatting + add new todo 2025-09-23 23:35:47 -04:00
neo
825e81a24f Merge pull request #3 from neoarz/readme+idevice 2025-09-23 23:34:12 -04:00
neoarz
153b9cedfc fix(readme): sizing and link 2025-09-23 23:32:35 -04:00
neoarz
ef7b6b9158 feat(readme): add download and update todo 2025-09-23 23:30:43 -04:00
neoarz
5f43289671 chore(readme): remove bs stuff 2025-09-23 23:08:40 -04:00
neoarz
acfc161322 chore(readme): test 2025-09-23 23:04:40 -04:00
neoarz
b90d621c4a feat(readme): more fixes plus install link 2025-09-23 23:03:21 -04:00
neoarz
f83357c88c feat(idevice): use json and make embed cool 2025-09-23 22:50:01 -04:00
neoarz
3c59dec58b feat(errorcodes): into json instead of py file 2025-09-23 21:56:26 -04:00
neoarz
9da3d935ae fix(errorcodes): fix embed formatting 2025-09-23 21:34:49 -04:00
neoarz
cd6e5fc47a feat(1): original add 2025-09-23 08:43:18 -04:00
neoarz
12d6c77228 fix(sidestore & idevice): update wording for no permissions 2025-09-23 08:18:24 -04:00
neo
f265591cd5 chore(todo): remove commands which would go in a self bot
project is coming to a close lol
2025-09-22 14:17:01 -04:00
neoarz
84d3e5df33 feat(owner): make owner commands be avaliable in dms 2025-09-22 10:22:06 -04:00
neoarz
deef9156fa fix(idevice): rename all commands to idevice 2025-09-22 09:11:05 -04:00
neoarz
e0147a683d fix(invite): remove permission addons
you can add permissions from the discord dev portal itself
2025-09-22 08:42:38 -04:00
neoarz
ede9168c9a fix(sidestore & idevice): error handling for missing permissions 2025-09-22 00:21:23 -04:00
neo
a5faf609fb chore(todo): finished graceful shutdown 2025-09-21 09:05:30 -04:00
neoarz
f28ae00d7d feat(shutdown): Improve bot shutdown handling and logging
Added graceful shutdown support via signals in bot.py, improved shutdown command to use SIGTERM, and enhanced logging for shutdown events. Updated UDID command description for clarity.
2025-09-21 08:47:15 -04:00
neoarz
2e818d3453 chore(help): add labubu commands to miscellaneous 2025-09-21 08:07:19 -04:00
neoarz
bed7d46f5b feat(udid): creates new udid command 2025-09-21 08:06:27 -04:00
neoarz
657452a553 feat(labubu): make code block ansi && add footer 2025-09-20 11:49:47 -04:00
neoarz
d51d8f1bc3 chore(gitignore): add .vscode 2025-09-20 11:21:05 -04:00
neoarz
5339a853b2 feat(labubu): make labubu command and give bot more permissions 2025-09-20 09:32:21 -04:00
neoarz
9793445d59 chore(todo): update todo 2025-09-20 08:55:11 -04:00
neoarz
53c4e742ef fix(half): remove method 2
Replaces the previous multi-step troubleshooting guide with a simplified list focused on ensuring users are on the latest version of SideStore and basic troubleshooting steps. Removes instructions for using an older version and streamlines the help message.
2025-09-20 08:27:56 -04:00
neoarz
301bafc435 feat(uptime): add uptime monitor with refreshes
Introduces an uptime command and view to show how long the bot has been running. Adds a method to calculate uptime in bot.py, registers the command in help.py, and implements the command logic in a new uptime.py cog with a refresh button.
2025-09-20 08:27:27 -04:00
neoarz
a9abf5488a chore(idevice && keanu): read description
Added 'idevice' as a help category and updated related command references for consistency. The Keanu command now displays a random image from a predefined list instead of a static image.
2025-09-19 20:39:49 -04:00
neoarz
461affb32e chore(todo): talk command 2025-09-19 20:16:00 -04:00
neoarz
a0a85d877a feat(idevice): create idevice category and first commands
Implemented the /idevice command with a dropdown for troubleshooting options and added the /noapps command to assist users when apps aren't showing in the installed apps view. Updated TODO.md to reflect completed tasks.
2025-09-19 20:14:02 -04:00
neoarz
8009c59d46 fix(keanu): set command to ephemeral=False
Expanded the TODO list with more detailed tasks and command breakdowns. Changed the keanu cog to send non-ephemeral messages for initial interaction responses.
2025-09-18 18:31:02 -04:00
neoarz
1c06f441c1 feat(miscellaneous): create new miscellaneous category
Introduces a new 'keanu' command under the miscellaneous category, displaying an embed with Keanu Reeves information. Updates help command to include the miscellaneous category and the new command.
2025-09-18 17:53:53 -04:00
neoarz
7cb0f9dec0 refactor(help): help command categories
Removed the 'template' category from help command suggestions and mappings, reorganized category mappings with comments, and cleaned up cogs/general/ping.py by removing header comments. Updated TODO.md with new tasks and future plans.
2025-09-18 17:33:27 -04:00
neoarz
63e49bcb7b feat(sidestore): create new afc command 2025-09-18 16:50:29 -04:00
neoarz
eba240302f chore(todo): create todo list
Introduces a TODO.md file outlining current and future development tasks for Syntrel, including idevice commands, unit tests, documentation, utility commands, tag system improvements, logging fixes, and webhook support.
2025-09-18 15:27:16 -04:00
neoarz
1d1d13dfd5 chore: update license section 2025-09-18 15:13:28 -04:00
neoarz
740dbffd52 chore: cleaner looking readme
Simplified the license section by consolidating third-party and original contribution details into a single note about dual licensing. Attribution and license details are now referenced in the LICENSE file.
2025-09-18 15:12:36 -04:00
neoarz
bbf5a0bb12 fix: ping command hanging 2025-09-17 23:04:20 -04:00
114 changed files with 10216 additions and 1357 deletions

View File

@@ -3,4 +3,10 @@ PREFIX=YOUR_BOT_PREFIX_HERE
INVITE_LINK=YOUR_BOT_INVITE_LINK_HERE
# Commands you want to disable (comma separated)
DISABLED_COGS=general.context_menus # Context Menus
DISABLED_COGS=general.context_menus
# yt-dlp cookies (for media/download command)
YTDLP_COOKIE_FILE=/bot/cogs/media/files/cookies.txt
# User IDs for co ownership (comma seperated)
OWNER_FRIENDS=0000000000,000000000,000000000

8
.gitignore vendored
View File

@@ -2,6 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
.ruff_cache/
# C extensions
*.so
@@ -140,8 +141,13 @@ cython_debug/
# PyCharm IDEA
.idea/*
# VS Code
.vscode/
# SQLITE database
*.db
# Log file
discord.log
discord.log
cogs/media/files/
logs.txt

1
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1 @@
print("Hello World")

View File

@@ -3,6 +3,8 @@ FROM python:3.12.9-slim-bookworm
WORKDIR /bot
COPY . /bot
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
RUN python -m pip install -r requirements.txt
ENTRYPOINT [ "python", "bot.py" ]

44
LICENSE
View File

@@ -1,27 +1,35 @@
Syntrel Discord Bot
MIT License
Copyright (c) 2025 neoarz
This project contains code from multiple sources under different licenses:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THIRD-PARTY SOFTWARE:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Python Discord Bot Template
Copyright © Krypton 2019-Present
Licensed under the Apache License, Version 2.0
Source: https://github.com/kkrypt0nn/Python-Discord-Bot-Template
The original Apache License, Version 2.0 applies to these components.
See [LICENSE-APACHE](licenses/LICENSE-APACHE) for the full license text.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
NEW CONTRIBUTIONS:
---
Original code and modifications by neoarz are licensed under the MIT License.
See [LICENSE-MIT](licenses/LICENSE-MIT) for the full license text.
ASSETS RESTRICTION NOTICE:
This includes but is not limited to:
Custom commands and cogs not present in the original template
Modifications and enhancements to existing functionality
New features and integrations
Documentation and configuration files created by neoarz
The following files and directories are NOT covered by the MIT License and are
restricted for use:
- assets/ (all files in this directory)
- Any image files, links, graphics, logos, or media files
- Any proprietary content or branding materials
These assets remain the exclusive property of neoarz and may not be used,
distributed, or modified without explicit written permission.

View File

@@ -8,21 +8,47 @@
<br>
> [!NOTE]
> **New Project**
>
> This project is still in the making!!! I would appreciate if you gave this repo a star, that would mean a lot to me!
<div align="center">
**All in one discord bot for helping with [SideStore](https://discord.gg/3DwCwpBHfv), [idevice](https://discord.gg/ZnNcrRT3M8), [MeloNX](https://discord.gg/Q4VkbkYfmk), and more.**
</div>
<br>
## Commands
![Total Commands](https://img.shields.io/badge/Total%20Commands-71-5865F2)
| Command&nbsp;group | Subcommands |
| ------------ | --- |
| ungrouped | `help`, `botinfo` |
| owner | `sync`, `cog_management`, `shutdown`, `say`, `invite`, `logs` |
| general | `serverinfo`, `ping`, `feedback`, `uptime`, `userinfo` |
| fun | `randomfact`, `coinflip`, `rps`, `8ball`, `minesweeper` |
| moderation | `kick`, `ban`, `nick`, `purge`, `hackban`, `warnings`, `archive`, `timeout` |
| sidestore | `help`, `refresh`, `code`, `crash`, `pairing`, `server`, `half`, `sparse`, `afc`, `udid` |
| idevice | `help`, `noapps`, `errorcode`, `developermode`, `mountddi` |
| melonx | `help`, `transfer`, `mods`, `gamecrash`, `requirements`, `error`, `26`, `legal` |
| events | `baitbot`, `stickybot` |
| miscellaneous | `keanu`, `labubu`, `piracy`, `tryitandsee`, `rickroll`, `dontasktoask`, `support`, `depart`, `docs` `sigma`, `duck`, `silly`, `color` |
| utilities | `translate`, `codepreview`, `dictionary` |
| media | `mcquote`, `img2gif`, `tweety`, `tts` |
## Licenses
## Download
#### Third-Party Components (Apache 2.0)
- Code derived from the [Python Discord Bot Template](https://github.com/kkrypt0nn/Python-Discord-Bot-Template) by [kkrypt0nn](https://github.com/kkrypt0nn) remains under the Apache License 2.0
- See [LICENSE-APACHE](licenses/LICENSE-APACHE) for the full Apache 2.0 license text
<div align="left">
<a href="https://discord.com/oauth2/authorize?client_id=1376728824108286034" target="_blank" rel="noopener noreferrer">
<img src="assets/download.png" alt="Download" style="width: 290px; height: auto;">
</a>
</div>
#### Original Contributions (MIT)
- New code, modifications, and enhancements by neoarz are licensed under the MIT License
- See [LICENSE-MIT](licenses/LICENSE-MIT) for the full MIT license text
## Contributing
See [LICENSE](LICENSE) file for detailed attribution information
Contributions are welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for guidelines.
## License
Dual licensed project, see [`LICENSE`](LICENSE) file for detailed attribution information

80
TODO.md Normal file
View File

@@ -0,0 +1,80 @@
# Todo List for Syntrel
- [x] Finish [idevice commands](https://github.com/jkcoxson/idevice/blob/master/idevice/src/lib.rs#L522)
- [x] Add /idevice command
- [x] Add /no apps command
- [x] Add rest of the errors yikes
- [x] Add ddi mounting command
- [x] Add melonx commands
- [x] Add /melonx command
- [x] Add /transfer command
- [x] Add /mods command
- [x] Add /gamecrash command
- [x] Add /requirements command
- [x] Add /error command
- [x] Add /26 command
- [ ] Add ai commands
- [ ] Add leaderboard command
- [ ] Add unit tests
- [ ] Add documentation to readme
- [ ] Finish CONTRIBUTING.md
- [ ] Create github action
- [x] Add video commands
- [ ] Add git log to info
- [ ] Move all assets to gh
- [x] Add codepreview for pull requests
- [x] Add try it and see command
- [x] ~~Add rickroll command~~
- [ ] Add letmegoogle command
- [ ] Add sending code (regular and from github)
- [x] Add random color command
- [x] Add translate command
- [ ] Add random picture command
- [ ] Add reminder command
- [ ] Add converting file types
- [ ] fix code preview command
- [ ] Add moderation giverole command
- [ ] add ~~admin abuse~~ command (for games)
- [ ] switch embeds to double embeds check [botinfo command](https://github.com/neoarz/Syntrel/blob/main/cogs/botinfo.py#L80)
- [ ] Add co-owner commands
- [ ] Add logs to channel
- [ ] make /codepreview into /github and it can parse any github url
- [ ] Add idevice livecontainer command
- [x] add [tts](https://developer.puter.com/tutorials/free-unlimited-text-to-speech-api/) command
- [x] update botinfo command
- [ ] Clean tag system from [tags branch](https://github.com/neoarz/Syntrel/tree/tags) (make sure db is persistent)
- [x] Fix logging and add graceful shutdown
- [x] Add uptime checker for the bot iself

BIN
assets/IMG_8453.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/discordbanner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
assets/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

BIN
assets/halloweenicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

261
bot.py
View File

@@ -1,9 +1,7 @@
import json
import logging
import os
import platform
import random
import sys
import time
import aiosqlite
import discord
@@ -12,22 +10,22 @@ from discord.ext.commands import Context
from dotenv import load_dotenv
from database import DatabaseManager
from utils import ascii, setup_logger, get_uptime, setup_signal_handlers
load_dotenv()
"""
Setup bot intents (events restrictions)
For more information about intents, please go to the following websites:
https://discordpy.readthedocs.io/en/latest/intents.html
https://discordpy.readthedocs.io/en/latest/intents.html#privileged-intents
Default Intents:
intents = discord.Intents.default()
intents.message_content = True
intents.bans = True
intents.dm_messages = True
intents.dm_reactions = True
intents.dm_typing = True
intents.emojis = True
intents.messages = True
intents.reactions = True
intents.typing = True
intents.voice_states = True
intents.webhooks = True
intents.emojis_and_stickers = True
intents.guild_messages = True
intents.guild_reactions = True
@@ -36,74 +34,11 @@ intents.guild_typing = True
intents.guilds = True
intents.integrations = True
intents.invites = True
intents.messages = True # `message_content` is required to get the content of the messages
intents.reactions = True
intents.typing = True
intents.voice_states = True
intents.webhooks = True
Privileged Intents (Needs to be enabled on developer portal of Discord), please use them only if you need them:
intents.members = True
intents.message_content = True
intents.presences = True
"""
intents = discord.Intents.default()
"""
Uncomment this if you want to use prefix (normal) commands.
It is recommended to use slash commands and therefore not use prefix commands.
If you want to use prefix commands, make sure to also enable the intent below in the Discord developer portal.
"""
intents.message_content = True
intents.members = True
class LoggingFormatter(logging.Formatter):
# Colors
black = "\x1b[30m"
red = "\x1b[31m"
green = "\x1b[32m"
yellow = "\x1b[33m"
blue = "\x1b[34m"
gray = "\x1b[38m"
# Styles
reset = "\x1b[0m"
bold = "\x1b[1m"
COLORS = {
logging.DEBUG: gray + bold,
logging.INFO: blue + bold,
logging.WARNING: yellow + bold,
logging.ERROR: red,
logging.CRITICAL: red + bold,
}
def format(self, record):
log_color = self.COLORS[record.levelno]
format = "(black){asctime}(reset) (levelcolor){levelname:<8}(reset) (green){name}(reset) {message}"
format = format.replace("(black)", self.black + self.bold)
format = format.replace("(reset)", self.reset)
format = format.replace("(levelcolor)", log_color)
format = format.replace("(green)", self.green + self.bold)
formatter = logging.Formatter(format, "%Y-%m-%d %H:%M:%S", style="{")
return formatter.format(record)
logger = logging.getLogger("discord_bot")
logger.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
console_handler.setFormatter(LoggingFormatter())
file_handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w")
file_handler_formatter = logging.Formatter(
"[{asctime}] [{levelname:<8}] {name}: {message}", "%Y-%m-%d %H:%M:%S", style="{"
)
file_handler.setFormatter(file_handler_formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger = setup_logger()
class DiscordBot(commands.Bot):
@@ -113,18 +48,12 @@ class DiscordBot(commands.Bot):
intents=intents,
help_command=None,
)
"""
This creates custom bot variables so that we can access these variables in cogs more easily.
For example, The logger is available using the following code:
- self.logger # In this class
- bot.logger # In this file
- self.bot.logger # In cogs
"""
self.logger = logger
self.database = None
self.bot_prefix = os.getenv("PREFIX")
self.invite_link = os.getenv("INVITE_LINK")
self.start_time = time.time()
self._shutdown = False
async def init_db(self) -> None:
async with aiosqlite.connect(
@@ -132,47 +61,66 @@ class DiscordBot(commands.Bot):
) as db:
with open(
f"{os.path.realpath(os.path.dirname(__file__))}/database/schema.sql",
encoding = "utf-8"
encoding="utf-8",
) as file:
await db.executescript(file.read())
await db.commit()
async def load_cogs(self) -> None:
"""
The code in this function is executed whenever the bot will start.
"""
cogs_path = f"{os.path.realpath(os.path.dirname(__file__))}/cogs"
disabled_env = os.getenv("DISABLED_COGS", "")
disabled_cogs = {entry.strip().lower() for entry in disabled_env.split(",") if entry.strip()}
disabled_cogs = {
entry.strip().lower() for entry in disabled_env.split(",") if entry.strip()
}
for folder in os.listdir(cogs_path):
folder_path = os.path.join(cogs_path, folder)
if os.path.isdir(folder_path) and not folder.startswith('__'):
for file in os.listdir(folder_path):
if file.endswith(".py") and not file.startswith('__'):
extension = file[:-3]
full_name = f"{folder}.{extension}".lower()
if extension.lower() in disabled_cogs or full_name in disabled_cogs:
self.logger.info(f"Skipped disabled extension '{full_name}'")
continue
try:
await self.load_extension(f"cogs.{folder}.{extension}")
self.logger.info(f"Loaded extension '{folder}.{extension}'")
except Exception as e:
exception = f"{type(e).__name__}: {e}"
self.logger.error(
f"Failed to load extension {folder}.{extension}\n{exception}"
)
if os.path.isdir(folder_path) and not folder.startswith("__"):
init_file = os.path.join(folder_path, "__init__.py")
if os.path.exists(init_file):
try:
await self.load_extension(f"cogs.{folder}")
if folder in ["owner", "help", "botinfo"]:
self.logger.info(f"Loaded extension '{folder}'")
except Exception as e:
exception = f"{type(e).__name__}: {e}"
self.logger.error(
f"Failed to load extension {folder}\n{exception}"
)
else:
for file in os.listdir(folder_path):
if file.endswith(".py") and not file.startswith("__"):
extension = file[:-3]
full_name = f"{folder}.{extension}".lower()
if (
extension.lower() in disabled_cogs
or full_name in disabled_cogs
):
self.logger.info(
f"Skipped disabled extension '{full_name}'"
)
continue
try:
await self.load_extension(f"cogs.{folder}.{extension}")
self.logger.info(
f"Loaded extension '{folder}.{extension}'"
)
except Exception as e:
exception = f"{type(e).__name__}: {e}"
self.logger.error(
f"Failed to load extension {folder}.{extension}\n{exception}"
)
for file in os.listdir(cogs_path):
if file.endswith(".py") and not file.startswith('__'):
if file.endswith(".py") and not file.startswith("__"):
extension = file[:-3]
if extension.lower() in disabled_cogs:
self.logger.info(f"Skipped disabled extension '{extension}'")
continue
try:
await self.load_extension(f"cogs.{extension}")
self.logger.info(f"Loaded extension '{extension}'")
if extension in ["owner", "help", "botinfo"]:
self.logger.info(f"Loaded extension '{extension}'")
except Exception as e:
exception = f"{type(e).__name__}: {e}"
self.logger.error(
@@ -181,30 +129,21 @@ class DiscordBot(commands.Bot):
@tasks.loop(minutes=1.0)
async def status_task(self) -> None:
"""
Setup the game status task of the bot.
"""
statuses = ["Sidestore", "MeloNX", "Armsx2", "Stikdebug", "Feather"]
statuses = ["SideStore", "MeloNX", "ARMSX2", "StikDebug", "Feather"]
await self.change_presence(activity=discord.Game(random.choice(statuses)))
@status_task.before_loop
async def before_status_task(self) -> None:
"""
Before starting the status changing task, we make sure the bot is ready
"""
await self.wait_until_ready()
async def setup_hook(self) -> None:
"""
This will just be executed when the bot starts the first time.
"""
self.logger.info(f"Logged in as {self.user.name}")
self.logger.info(f"discord.py API version: {discord.__version__}")
self.logger.info(f"Python version: {platform.python_version()}")
self.logger.info(
f"Running on: {platform.system()} {platform.release()} ({os.name})"
)
try:
app_info = await self.application_info()
if app_info.team:
@@ -212,10 +151,12 @@ class DiscordBot(commands.Bot):
for member in app_info.team.members:
self.logger.info(f"Team member: {member.name} (ID: {member.id})")
else:
self.logger.info(f"Bot owner: {app_info.owner.name} (ID: {app_info.owner.id})")
self.logger.info(
f"Bot owner: {app_info.owner.name} (ID: {app_info.owner.id})"
)
except Exception as e:
self.logger.error(f"Error fetching application info: {e}")
await self.init_db()
await self.load_cogs()
self.status_task.start()
@@ -225,41 +166,41 @@ class DiscordBot(commands.Bot):
)
)
async def on_message(self, message: discord.Message) -> None:
"""
The code in this event is executed every time someone sends a message, with or without the prefix
def get_uptime(self) -> str:
return get_uptime(self.start_time)
:param message: The message that was sent.
"""
if message.author == self.user or message.author.bot:
async def close(self) -> None:
if self._shutdown:
return
# Reacts to messages which mention the bot
if self.user in message.mentions:
self._shutdown = True
self.logger.info("Starting shutdown process...")
if self.status_task and not self.status_task.is_being_cancelled():
self.status_task.cancel()
self.logger.info("Status task cancelled")
if self.database and self.database.connection:
try:
emoji_string = "<a:PandaPing:1417550314260926575>"
self.logger.debug(f"Attempting to react with PandaPing emoji: {emoji_string}")
await message.add_reaction(emoji_string)
self.logger.debug("Successfully reacted with PandaPing emoji")
await self.database.connection.close()
self.logger.warning("Database connection closed")
except Exception as e:
self.logger.debug(f"Failed to react with PandaPing emoji: {e}")
try:
self.logger.debug("Falling back to wave emoji")
await message.add_reaction("👋")
self.logger.debug("Successfully reacted with wave emoji")
except Exception as fallback_error:
self.logger.debug(f"Failed to react with fallback emoji: {fallback_error}")
await self.process_commands(message)
self.logger.error(f"Error closing database connection: {e}")
try:
await super().close()
self.logger.critical("Bot shutdown complete")
except Exception as e:
self.logger.error(f"Error during bot shutdown: {e}")
async def on_command_completion(self, context: Context) -> None:
"""
The code in this event is executed every time a normal command has been *successfully* executed.
:param context: The context of the command that has been executed.
"""
full_command_name = context.command.qualified_name
split = full_command_name.split(" ")
executed_command = str(split[0])
if executed_command.lower() in ["shutdown", "say", "embed"]:
return
if context.guild is not None:
self.logger.info(
f"Executed {executed_command} command in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id})"
@@ -270,12 +211,6 @@ class DiscordBot(commands.Bot):
)
async def on_command_error(self, context: Context, error) -> None:
"""
The code in this event is executed every time a normal valid command catches an error.
:param context: The context of the normal command that failed executing.
:param error: The error that has been faced.
"""
if isinstance(error, commands.CommandNotFound):
if context.guild is not None:
self.logger.info(
@@ -323,14 +258,28 @@ class DiscordBot(commands.Bot):
elif isinstance(error, commands.MissingRequiredArgument):
embed = discord.Embed(
title="Error!",
# We need to capitalize because the command arguments have no capital letter in the code and they are the first word in the error message.
description=str(error).capitalize(),
color=0xE02B2B,
)
await context.send(embed=embed)
elif isinstance(error, commands.CheckFailure):
return
else:
raise error
bot = DiscordBot()
bot.run(os.getenv("TOKEN"))
if __name__ == "__main__":
os.system("clear" if os.name == "posix" else "cls")
print(ascii)
setup_signal_handlers(bot)
try:
bot.run(os.getenv("TOKEN"))
except KeyboardInterrupt:
logger.info("Received keyboard interrupt")
except Exception as e:
logger.critical(f"Fatal error during bot execution: {type(e).__name__}: {e}")

210
cogs/botinfo.py Normal file
View File

@@ -0,0 +1,210 @@
import platform
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from datetime import datetime
import pytz
from utils.contributors import generate_contributors_image
class FeedbackForm(discord.ui.Modal, title="Feedback"):
feedback = discord.ui.TextInput(
label="What do you think about this bot?",
style=discord.TextStyle.long,
placeholder="Type your answer here...",
required=True,
max_length=256,
)
def __init__(self, bot):
super().__init__()
self.bot = bot
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.send_message(
embed=discord.Embed(
title="Thank You!",
description="Your feedback has been submitted, the owners have been notified about it.",
color=0x7289DA,
).set_author(
name="Feedback System",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
),
ephemeral=True,
)
app_owner = (await self.bot.application_info()).owner
await app_owner.send(
embed=discord.Embed(
title="New Feedback",
description=f"{interaction.user} (<@{interaction.user.id}>) has submitted a new feedback:\n```\n{self.feedback.value}\n```",
color=0x7289DA,
).set_author(
name="Feedback System",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
)
)
class BotInfoView(discord.ui.View):
def __init__(self, bot):
super().__init__(timeout=None)
self.bot = bot
github_emoji = discord.PartialEmoji(name="githubicon", id=1417717356846776340)
github_button = discord.ui.Button(
label="GitHub",
emoji=github_emoji,
url="https://github.com/neoarz/syntrel",
style=discord.ButtonStyle.link,
)
self.add_item(github_button)
feedback_emoji = discord.PartialEmoji(
name="ThumbsUpBlueEmoji", id=1426066711500554302
)
feedback_button = discord.ui.Button(
label="Feedback",
emoji=feedback_emoji,
style=discord.ButtonStyle.secondary,
custom_id="feedback_button",
)
feedback_button.callback = self.feedback_callback
self.add_item(feedback_button)
bug_emoji = discord.PartialEmoji(name="BugHunterBadge", id=1425703361625460856)
bug_button = discord.ui.Button(
label="Bug Report",
emoji=bug_emoji,
url="https://github.com/neoarz/Syntrel/issues",
style=discord.ButtonStyle.link,
)
self.add_item(bug_button)
async def feedback_callback(self, interaction: discord.Interaction):
feedback_form = FeedbackForm(self.bot)
await interaction.response.send_modal(feedback_form)
class BotInfo(commands.Cog, name="botinfo"):
def __init__(self, bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild):
channel = guild.system_channel or next(
(
c
for c in guild.text_channels
if c.permissions_for(guild.me).send_messages
),
None,
)
if channel:
ny_tz = pytz.timezone("America/New_York")
current_time = datetime.now(ny_tz).strftime("%m/%d/%y, %I:%M %p")
description_text = (
'Heyooo! I\'m Syntrel, a bot made to help with [SideStore](https://discord.gg/3DwCwpBHfv), [MeloNX](https://discord.gg/Q4VkbkYfmk), and [idevice](https://discord.gg/ZnNcrRT3M8). I even have some cool extras! If you encounter any issues, please file a bug report. If you have any feedback or suggestions, simply select "Feedback"! <:HeardPanda:1417619745896660992>\n\n'
"**New to Syntrel?** Run `/help` to get started and explore all available commands!\n\n"
f"**Owner:** [neoarz](https://discordapp.com/users/1015372540937502851)\n"
f"**Python Version:** {platform.python_version()}\n"
f"**Discord.py Version:** {discord.__version__}\n"
f"**Prefix:** / (Slash Commands) or {self.bot.bot_prefix} for normal commands"
)
embed = discord.Embed(
title="Syntrel Discord Bot",
description=description_text,
color=0x7289DA,
)
embed.set_author(
name="Syntrel",
icon_url="https://github.com/neoarz/Syntrel/blob/main/assets/icon.png?raw=true",
)
embed.set_image(
url="https://github.com/neoarz/Syntrel/raw/main/assets/bannerdark.png"
)
embed.set_footer(
text=f"neoarz • {current_time}",
icon_url="https://yes.nighty.works/raw/P1Us35.webp",
)
view = BotInfoView(self.bot)
await channel.send(embed=embed, view=view)
else:
self.bot.logger.warning(f"Couldn't find a suitable channel in {guild.name}")
@commands.hybrid_command(
name="botinfo",
description="Get some useful (or not) information about the bot.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
async def botinfo(self, context: Context) -> None:
if context.interaction:
await context.interaction.response.defer(ephemeral=False)
ny_tz = pytz.timezone("America/New_York")
current_time = datetime.now(ny_tz).strftime("%m/%d/%y, %I:%M %p")
description_text = (
'Heyooo! I\'m Syntrel, a bot made to help with [SideStore](https://discord.gg/3DwCwpBHfv), [MeloNX](https://discord.gg/Q4VkbkYfmk), and [idevice](https://discord.gg/ZnNcrRT3M8). I even have some cool extras! If you encounter any issues, please file a bug report. If you have any feedback or suggestions, simply select "Feedback"! <:HeardPanda:1417619745896660992>\n\n'
"**New to Syntrel?** Run `/help` to get started and explore all available commands!\n\n"
f"**Owner:** [neoarz](https://discordapp.com/users/1015372540937502851)\n"
f"**Python Version:** {platform.python_version()}\n"
f"**Discord.py Version:** {discord.__version__}\n"
f"**Prefix:** / (Slash Commands) or {self.bot.bot_prefix} for normal commands"
)
embed1 = discord.Embed(
title="Syntrel Discord Bot",
description=description_text,
color=0x7289DA,
)
embed1.set_author(
name="Syntrel",
icon_url="https://github.com/neoarz/Syntrel/blob/main/assets/icon.png?raw=true",
)
embed1.set_image(
url="https://github.com/neoarz/Syntrel/raw/main/assets/bannerdark.png"
)
embed1.set_footer(
text=f"neoarz • {current_time}",
icon_url="https://yes.nighty.works/raw/P1Us35.webp",
)
embed2 = discord.Embed(
title="Contributors",
description="Giving credit where it's due! <a:pandasquish:1428617277317709915>",
color=0x7289DA,
)
contributors_image = generate_contributors_image()
view = BotInfoView(self.bot)
if contributors_image:
file = discord.File(contributors_image, filename="contributors.png")
embed2.set_image(url="attachment://contributors.png")
if context.interaction:
await context.interaction.followup.send(
embeds=[embed1, embed2], file=file, view=view
)
else:
await context.send(embeds=[embed1, embed2], file=file, view=view)
else:
if context.interaction:
await context.interaction.followup.send(
embeds=[embed1, embed2], view=view
)
else:
await context.send(embeds=[embed1, embed2], view=view)
async def setup(bot) -> None:
await bot.add_cog(BotInfo(bot))

95
cogs/events/__init__.py Normal file
View File

@@ -0,0 +1,95 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .baitbot import baitbot_command, BaitBotListener, has_protected_role
from .mention import MentionListener
from .stickybot import (
stickybot_command,
StickyBotListener,
has_allowed_role as has_sticky_role,
)
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Events(commands.GroupCog, name="events"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="events", invoke_without_command=True)
async def events_group(self, context: Context):
embed = discord.Embed(
title="Events Commands",
description="Use `.events <subcommand>` or `/events <subcommand>`.",
color=0x7289DA,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
embed.add_field(name="Available", value="baitbot, stickybot", inline=False)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str, **kwargs):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command, **kwargs)
else:
await context.send(f"Unknown events command: {name}")
@events_group.command(name="baitbot")
@has_protected_role()
async def events_group_baitbot(self, context: Context):
await self._invoke_hybrid(context, "baitbot")
@events_group.command(name="stickybot")
@has_sticky_role()
async def events_group_stickybot(self, context: Context):
await self._invoke_hybrid(context, "stickybot")
@commands.check(_require_group_prefix)
@has_protected_role()
@commands.hybrid_command(
name="baitbot", description="View bait bot configuration and status."
)
async def baitbot(self, context):
return await baitbot_command()(self, context)
@commands.check(_require_group_prefix)
@has_sticky_role()
@commands.hybrid_command(
name="stickybot", description="View sticky bot configuration and status."
)
async def stickybot(self, context):
return await stickybot_command()(self, context)
async def setup(bot) -> None:
cog = Events(bot)
await bot.add_cog(cog)
listener = BaitBotListener(bot)
await bot.add_cog(listener)
mention_listener = MentionListener(bot)
await bot.add_cog(mention_listener)
sticky_bot = StickyBotListener(bot)
await bot.add_cog(sticky_bot)
bot.logger.info("Loaded extension 'events.baitbot'")
bot.logger.info("Loaded extension 'events.mention'")
bot.logger.info("Loaded extension 'events.stickybot'")

365
cogs/events/baitbot.py Normal file
View File

@@ -0,0 +1,365 @@
import discord
from discord.ext import commands
from discord.ext.commands import Context
import asyncio
# Make a pr to add your own server config here, you shouldn't need to touch the rest of the file, please fill in all the values for your own server
BAIT_CONFIGS = {
"SideStore": {
"guild_id": 949183273383395328,
"channel_ids": [
1432134748062482586,
1432204211009097819,
],
"protected_role_id": 959598279685963776,
"log_channel_id": 1433532504647667823,
},
"neotest": {
"guild_id": 1069946178659160076,
"channel_ids": [
1432175690270118012,
1433987189670281278,
1433988339031080991,
],
"protected_role_id": 1432165329483857940,
"log_channel_id": 1433987853184139365,
},
"idevice": {
"guild_id": 1329314147434758175,
"channel_ids": [
1434317669695492177,
],
"protected_role_id": 1333666918548111373,
"log_channel_id": 1333673259446571022,
},
"melonx": {
"guild_id": 1300369899704680479,
"channel_ids": [
1434327970679492830,
],
"protected_role_id": 1300372178138697758,
"log_channel_id": 1300374786471366696,
},
}
BAN_REASON = "Detected bot/scammer in bait channel"
def has_protected_role():
async def predicate(context: Context):
if not context.guild:
context.bot.logger.warning(
f"[BAITBOT] Unauthorized baitbot command attempt by {context.author} ({context.author.id}) in DMs"
)
embed = discord.Embed(
title="Permission Denied",
description="You don't have permission to use this command.",
color=0xE02B2B,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
await context.send(embed=embed, ephemeral=True)
return False
if not hasattr(context.author, "roles"):
context.bot.logger.warning(
f"[BAITBOT] Unauthorized baitbot command attempt by {context.author} ({context.author.id}) in {context.guild.name} - no roles"
)
embed = discord.Embed(
title="Permission Denied",
description="You don't have permission to use this command.",
color=0xE02B2B,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
await context.send(embed=embed, ephemeral=True)
return False
for config in BAIT_CONFIGS.values():
protected_role_id = config.get("protected_role_id")
if protected_role_id:
protected_role = context.guild.get_role(protected_role_id)
if protected_role:
for role in context.author.roles:
if (
role.position >= protected_role.position
and role.id != context.guild.default_role.id
):
return True
context.bot.logger.warning(
f"[BAITBOT] Unauthorized baitbot command attempt by {context.author} ({context.author.id}) in {context.guild.name} - insufficient role permissions"
)
embed = discord.Embed(
title="Permission Denied",
description="You don't have permission to use this command.",
color=0xE02B2B,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
await context.send(embed=embed, ephemeral=True)
return False
return commands.check(predicate)
def baitbot_command():
async def wrapper(self, context: Context):
embed = discord.Embed(
title="Bait Bot",
description="Bans people who post in a specific channel.",
color=0x7289DA,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
found_config = False
if BAIT_CONFIGS:
for name, config in BAIT_CONFIGS.items():
guild_id = config.get("guild_id")
if context.guild and guild_id == context.guild.id:
channel_ids = config.get("channel_ids", [])
if not channel_ids:
channel_id = config.get("channel_id")
if channel_id:
channel_ids = [channel_id]
role_id = config.get("protected_role_id", "Not set")
channel_displays = []
for channel_id in channel_ids:
channel = context.guild.get_channel(channel_id)
channel_display = (
f"<#{channel_id}> (`{channel_id}`)"
if channel
else f"`{channel_id}`"
)
channel_displays.append(channel_display)
channels_text = (
"\n".join(channel_displays) if channel_displays else "Not set"
)
role = context.guild.get_role(role_id)
role_display = (
f"<@&{role_id}> (`{role_id}`)" if role else f"`{role_id}`"
)
log_channel_id = config.get("log_channel_id")
log_channel = None
if log_channel_id:
log_channel = context.guild.get_channel(log_channel_id)
log_display = (
f"<#{log_channel_id}> (`{log_channel_id}`)"
if log_channel
else (f"`{log_channel_id}`" if log_channel_id else "Not set")
)
embed.add_field(
name="\u200b",
value=f"Channels:\n{channels_text}\n\nProtected Role:\n{role_display}\n\nLog Channel:\n{log_display}",
inline=False,
)
found_config = True
if not found_config:
embed.add_field(
name="No Configurations",
value="No bait channels configured for this server",
inline=False,
)
if context.guild and context.guild.icon:
embed.set_thumbnail(url=context.guild.icon.url)
await context.send(embed=embed)
return wrapper
class BaitBotListener(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.guild is None:
return
if message.author.bot:
return
bait_config = None
config_name = None
for name, config in BAIT_CONFIGS.items():
if message.guild.id != config.get("guild_id"):
continue
channel_ids = config.get("channel_ids", [])
if not channel_ids:
channel_id = config.get("channel_id")
if channel_id:
channel_ids = [channel_id]
if message.channel.id in channel_ids:
bait_config = config
config_name = name
break
if not bait_config:
return
protected_role_id = bait_config.get("protected_role_id")
is_protected = False
if protected_role_id and hasattr(message.author, "roles"):
protected_role = message.guild.get_role(protected_role_id)
if protected_role:
for role in message.author.roles:
if (
role.position >= protected_role.position
and role.id != message.guild.default_role.id
):
self.bot.logger.info(
f"[BAITBOT] Skipped banning {message.author} ({message.author.id}) in #{message.channel.name}: protected role ({role.name})"
)
is_protected = True
break
message_content = message.content if message.content else "*No text content*"
message_attachments = message.attachments
try:
await message.delete()
self.bot.logger.info(
f"[BAITBOT] Deleted message from {message.author} in #{message.channel.name}"
)
except Exception as e:
self.bot.logger.warning(
f"[BAITBOT] Could not delete message from {message.author}: {e}"
)
banned = False
if not is_protected:
try:
self.bot.logger.warning(
f"[BAITBOT] Detected user in bait channel [{config_name}]: {message.author.name} ({message.author.id}) in #{message.channel.name}"
)
if not message.guild.me.guild_permissions.ban_members:
self.bot.logger.error(
f"[BAITBOT] No permission to ban members in {message.guild.name}"
)
else:
try:
await message.author.ban(
reason=BAN_REASON, delete_message_days=7
)
self.bot.logger.info(
f"[BAITBOT] Banned {message.author.name} - deleted messages from last 7 days"
)
banned = True
except discord.Forbidden:
self.bot.logger.error(
f"[BAITBOT] Could not ban {message.author.name}: missing permissions"
)
except Exception as e:
self.bot.logger.error(
f"[BAITBOT] Error banning {message.author.name}: {e}"
)
if banned:
await asyncio.sleep(2)
try:
await message.guild.unban(
message.author, reason="Auto-unban after cleanup"
)
self.bot.logger.info(
f"[BAITBOT] Unbanned {message.author.name} - cleanup complete"
)
except Exception as e:
self.bot.logger.error(
f"[BAITBOT] Error unbanning {message.author.name}: {e}"
)
except Exception as e:
self.bot.logger.error(f"[BAITBOT] Error handling bait message: {e}")
log_channel_id = bait_config.get("log_channel_id")
if log_channel_id:
try:
log_channel = self.bot.get_channel(log_channel_id)
if log_channel:
action_text = (
"Message deleted (user banned and unbanned)"
if banned
else "Message deleted (protected user)"
if is_protected
else "Message deleted"
)
log_embed = discord.Embed(
title="Bait Bot",
description=action_text,
color=0xE02B2B,
timestamp=message.created_at,
)
log_embed.set_author(
name=str(message.author),
icon_url=message.author.display_avatar.url,
)
log_embed.add_field(
name="User", value=message.author.mention, inline=True
)
log_embed.add_field(
name="Channel", value=message.channel.mention, inline=True
)
combined_content = []
if message_content and message_content != "*No text content*":
combined_content.append(message_content)
image_url = None
if message_attachments:
for attachment in message_attachments:
if (
attachment.content_type
and attachment.content_type.startswith("image/")
):
if not image_url:
image_url = attachment.url
combined_content.append(attachment.filename)
content_text = (
"\n".join(combined_content)
if combined_content
else "*No content*"
)
if len(content_text) > 1000:
content_text = content_text[:997] + "..."
log_embed.add_field(
name="Content", value=f"```\n{content_text}\n```", inline=False
)
if image_url:
log_embed.set_image(url=image_url)
log_embed.set_footer(text=f"Message ID: {message.id}")
try:
await log_channel.send(embed=log_embed)
self.bot.logger.info(
f"[BAITBOT] Sent log to #{log_channel.name}"
)
except discord.Forbidden:
self.bot.logger.error(
f"[BAITBOT] No permission to send log to #{log_channel.name}"
)
except Exception as e:
self.bot.logger.error(f"[BAITBOT] Error sending log: {e}")
else:
self.bot.logger.warning(
f"[BAITBOT] Log channel {log_channel_id} not found"
)
except Exception as e:
self.bot.logger.error(f"[BAITBOT] Error handling log channel: {e}")

37
cogs/events/mention.py Normal file
View File

@@ -0,0 +1,37 @@
import discord
import random
from discord.ext import commands
class MentionListener(commands.Cog):
def __init__(self, bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
if message.author == self.bot.user or message.author.bot:
return
if self.bot.user in message.mentions:
try:
emoji_options = [
"<a:PandaPing:1417550314260926575>",
"<:PandaPing2:1434998389451395224>",
"<:PandaPing3:1434998524696723466>",
]
selected_emoji = random.choice(emoji_options)
self.bot.logger.debug(
f"Attempting to react with emoji: {selected_emoji}"
)
await message.add_reaction(selected_emoji)
self.bot.logger.debug("Successfully reacted with mention emoji")
except Exception as e:
self.bot.logger.debug(f"Failed to react with mention emoji: {e}")
try:
self.bot.logger.debug("Falling back to wave emoji")
await message.add_reaction("👋")
self.bot.logger.debug("Successfully reacted with wave emoji")
except Exception as fallback_error:
self.bot.logger.debug(
f"Failed to react with fallback emoji: {fallback_error}"
)

310
cogs/events/stickybot.py Normal file
View File

@@ -0,0 +1,310 @@
import discord
from discord.ext import commands
from discord.ext.commands import Context
import asyncio
# Make a pr to add your own server config here, you shouldn't need to touch the rest of the file, please fill in all the values for your own server
STICKY_CONFIGS = {
"neotest": {
"guild_id": 1069946178659160076,
"channel_ids": [
1455338488546459789,
],
"allowed_role_id": 1432165329483857940,
"message": "# Example sticky message", # You can add your own markdown here
"footer": "This is an automated sticky message.", # This will be appended to the message and uses "-#" to format the footer
"delay": 5, # in seconds
},
"SideStore": {
"guild_id": 949183273383395328,
"channel_ids": [
1279548738586673202,
],
"allowed_role_id": 949207813815697479,
"message": "## Please read the README in https://discord.com/channels/949183273383395328/1155736594679083089 and the documentation at <https://docs.sidestore.io> before asking your question.",
"footer": "This is an automated sticky message.",
"delay": 5,
},
}
def has_allowed_role():
async def predicate(context: Context):
if not context.guild:
context.bot.logger.warning(
f"[STICKYBOT] Unauthorized stickybot command attempt by {context.author} ({context.author.id}) in DMs"
)
embed = discord.Embed(
title="Permission Denied",
description="You don't have permission to use this command.",
color=0xE02B2B,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
await context.send(embed=embed, ephemeral=True)
return False
if not hasattr(context.author, "roles"):
context.bot.logger.warning(
f"[STICKYBOT] Unauthorized stickybot command attempt by {context.author} ({context.author.id}) in {context.guild.name} - no roles"
)
embed = discord.Embed(
title="Permission Denied",
description="You don't have permission to use this command.",
color=0xE02B2B,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
await context.send(embed=embed, ephemeral=True)
return False
for config in STICKY_CONFIGS.values():
if context.guild.id != config.get("guild_id"):
continue
allowed_role_id = config.get("allowed_role_id")
if allowed_role_id:
allowed_role = context.guild.get_role(allowed_role_id)
if allowed_role:
for role in context.author.roles:
if (
role.position >= allowed_role.position
and role.id != context.guild.default_role.id
):
return True
context.bot.logger.warning(
f"[STICKYBOT] Unauthorized stickybot command attempt by {context.author} ({context.author.id}) in {context.guild.name} - insufficient role permissions"
)
embed = discord.Embed(
title="Permission Denied",
description="You don't have permission to use this command.",
color=0xE02B2B,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
await context.send(embed=embed, ephemeral=True)
return False
return commands.check(predicate)
def stickybot_command():
async def wrapper(self, context: Context):
embed = discord.Embed(
title="Sticky Bot",
description="Sends sticky messages in configured channels.",
color=0x7289DA,
)
embed.set_author(
name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png"
)
found_config = False
if STICKY_CONFIGS:
for name, config in STICKY_CONFIGS.items():
guild_id = config.get("guild_id")
if context.guild and guild_id == context.guild.id:
channel_ids = config.get("channel_ids", [])
channel_displays = []
for channel_id in channel_ids:
channel = context.guild.get_channel(channel_id)
channel_display = (
f"<#{channel_id}> (`{channel_id}`)"
if channel
else f"`{channel_id}`"
)
channel_displays.append(channel_display)
channels_text = (
"\n".join(channel_displays) if channel_displays else "Not set"
)
allowed_role_id = config.get("allowed_role_id", "Not set")
role = context.guild.get_role(allowed_role_id)
role_display = (
f"<@&{allowed_role_id}> (`{allowed_role_id}`)"
if role
else f"`{allowed_role_id}`"
)
message_content = config.get("message", "*No message set*")
footer_text = config.get("footer", "This is an automated sticky message.")
full_content = f"{message_content}\n-# {footer_text}"
embed.add_field(
name="\u200b",
value=f"**Channels:**\n{channels_text}\n\n**Allowed Role:**\n{role_display}\n\n**Message Preview:**\n```\n{full_content}\n```",
inline=False,
)
found_config = True
if not found_config:
embed.add_field(
name="No Configurations",
value="No sticky configurations found for this server",
inline=False,
)
if context.guild and context.guild.icon:
embed.set_thumbnail(url=context.guild.icon.url)
await context.send(embed=embed)
return wrapper
class StickyBotListener(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.last_sticky_messages = {}
self.debounce_tasks = {}
async def delete_last_sticky(self, channel):
try:
active_config = None
for config in STICKY_CONFIGS.values():
if channel.guild.id == config.get(
"guild_id"
) and channel.id in config.get("channel_ids", []):
active_config = config
break
if not active_config:
return
target_footer = active_config.get(
"footer", "This is an automated sticky message."
)
async for message in channel.history(limit=20):
if (
message.author.id == self.bot.user.id
and target_footer in message.content
):
await message.delete()
except Exception as e:
self.bot.logger.warning(
f"[STICKYBOT] Error cleaning up sticky in #{channel.name}: {e}"
)
async def send_sticky_message(self, channel, config):
if not channel:
return
last_msg_id = self.last_sticky_messages.get(channel.id)
deleted = False
if last_msg_id:
try:
old_msg = await channel.fetch_message(last_msg_id)
await old_msg.delete()
deleted = True
except discord.NotFound:
deleted = True
except discord.Forbidden:
self.bot.logger.warning(
f"[STICKYBOT] Missing delete permissions in #{channel.name}"
)
except Exception as e:
self.bot.logger.warning(
f"[STICKYBOT] Error deleting info in #{channel.name}: {e}"
)
if not deleted:
await self.delete_last_sticky(channel)
message_content = config.get("message")
if not message_content:
return
footer_text = config.get("footer", "This is an automated sticky message.")
footer = f"\n-# {footer_text}"
full_content = f"{message_content}{footer}"
try:
new_msg = await channel.send(
full_content, allowed_mentions=discord.AllowedMentions.none()
)
self.last_sticky_messages[channel.id] = new_msg.id
except discord.Forbidden:
self.bot.logger.warning(
f"[STICKYBOT] Missing send permissions in #{channel.name}"
)
except Exception as e:
self.bot.logger.error(
f"[STICKYBOT] Error sending sticky in #{channel.name}: {e}"
)
@commands.Cog.listener()
async def on_ready(self):
await self.bot.wait_until_ready()
await self.initialize_stickies()
async def initialize_stickies(self):
for name, config in STICKY_CONFIGS.items():
guild_id = config.get("guild_id")
guild = self.bot.get_guild(guild_id)
if not guild:
continue
channel_ids = config.get("channel_ids", [])
for channel_id in channel_ids:
channel = guild.get_channel(channel_id)
if channel:
await self.send_sticky_message(channel, config)
async def trigger_sticky(self, channel, guild):
if not guild or not channel:
return
active_config = None
for config in STICKY_CONFIGS.values():
if guild.id == config.get("guild_id"):
if channel.id in config.get("channel_ids", []):
active_config = config
break
if not active_config:
return
channel_id = channel.id
if channel_id in self.debounce_tasks:
self.debounce_tasks[channel_id].cancel()
async def debounce_wrapper():
try:
delay = active_config.get("delay", 5)
await asyncio.sleep(delay)
await self.send_sticky_message(channel, active_config)
except asyncio.CancelledError:
pass
except Exception as e:
self.bot.logger.error(f"[STICKYBOT] Error in debounce task: {e}")
finally:
if self.debounce_tasks.get(channel_id) == asyncio.current_task():
del self.debounce_tasks[channel_id]
self.debounce_tasks[channel_id] = self.bot.loop.create_task(debounce_wrapper())
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.guild is None or message.author.bot:
return
if message.id == self.last_sticky_messages.get(message.channel.id):
return
await self.trigger_sticky(message.channel, message.guild)
@commands.Cog.listener()
async def on_interaction(self, interaction: discord.Interaction):
if interaction.guild is None or interaction.user.bot:
return
if interaction.type == discord.InteractionType.application_command:
await self.trigger_sticky(interaction.channel, interaction.guild)

117
cogs/fun/__init__.py Normal file
View File

@@ -0,0 +1,117 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .coinflip import coinflip_command
from .eightball import eightball_command
from .minesweeper import minesweeper_command
from .randomfact import randomfact_command
from .rockpaperscissors import rps_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Fun(commands.GroupCog, name="fun"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="fun", invoke_without_command=True)
async def fun_group(self, context: Context):
embed = discord.Embed(
title="Fun Commands",
description="Use `.fun <subcommand>` or slash `/fun <subcommand>`.",
color=0x7289DA,
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
embed.add_field(
name="Available",
value="coinflip, 8ball, minesweeper, randomfact, rps",
inline=False,
)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str, **kwargs):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command, **kwargs)
else:
await context.send(f"Unknown fun command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@fun_group.command(name="coinflip")
async def fun_group_coinflip(self, context: Context):
await self._invoke_hybrid(context, "coinflip")
@fun_group.command(name="8ball")
async def fun_group_8ball(self, context: Context, *, question: str):
await self._invoke_hybrid(context, "8ball", question=question)
@fun_group.command(name="minesweeper")
async def fun_group_minesweeper(self, context: Context):
await self._invoke_hybrid(context, "minesweeper")
@fun_group.command(name="randomfact")
async def fun_group_randomfact(self, context: Context):
await self._invoke_hybrid(context, "randomfact")
@fun_group.command(name="rps")
async def fun_group_rps(self, context: Context):
await self._invoke_hybrid(context, "rps")
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="coinflip", description="Make a coin flip, but give your bet before."
)
async def coinflip(self, context):
return await coinflip_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="8ball",
description="Ask any question to the bot.",
)
async def eight_ball(self, context, *, question: str):
return await eightball_command()(self, context, question=question)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="minesweeper", description="Play a buttoned minesweeper mini-game."
)
async def minesweeper(self, context):
return await minesweeper_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="randomfact", description="Get a random fact.")
async def randomfact(self, context):
return await randomfact_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="rps", description="Play the rock paper scissors game against the bot."
)
async def rock_paper_scissors(self, context):
return await rps_command()(self, context)
async def setup(bot) -> None:
cog = Fun(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'fun.coinflip'")
bot.logger.info("Loaded extension 'fun.8ball'")
bot.logger.info("Loaded extension 'fun.minesweeper'")
bot.logger.info("Loaded extension 'fun.randomfact'")
bot.logger.info("Loaded extension 'fun.rps'")

View File

@@ -1,7 +1,7 @@
import random
import discord
from discord.ext import commands
from discord.ext.commands import Context
class Choice(discord.ui.View):
def __init__(self) -> None:
@@ -22,25 +22,19 @@ class Choice(discord.ui.View):
self.value = "tails"
self.stop()
class CoinFlip(commands.Cog, name="coinflip"):
def __init__(self, bot) -> None:
self.bot = bot
def coinflip_command():
@commands.hybrid_command(
name="coinflip", description="Make a coin flip, but give your bet before."
)
async def coinflip(self, context: Context) -> None:
"""
Make a coin flip, but give your bet before.
:param context: The hybrid command context.
"""
async def coinflip(self, context):
buttons = Choice()
embed = discord.Embed(
title="Coinflip",
description="What is your bet?",
color=0x7289DA
title="Coinflip", description="What is your bet?", color=0x7289DA
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
message = await context.send(embed=embed, view=buttons)
await buttons.wait()
result = random.choice(["heads", "tails"])
@@ -50,15 +44,18 @@ class CoinFlip(commands.Cog, name="coinflip"):
description=f"Correct! You guessed `{buttons.value}` and I flipped the coin to `{result}`.",
color=0x00FF00,
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
else:
embed = discord.Embed(
title="Coinflip",
description=f"Woops! You guessed `{buttons.value}` and I flipped the coin to `{result}`, better luck next time!",
color=0xE02B2B,
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
await message.edit(embed=embed, view=None, content=None)
async def setup(bot) -> None:
await bot.add_cog(CoinFlip(bot))
return coinflip

View File

@@ -1,26 +1,14 @@
import random
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class EightBall(commands.Cog, name="8ball"):
def __init__(self, bot) -> None:
self.bot = bot
def eightball_command():
@commands.hybrid_command(
name="8ball",
description="Ask any question to the bot.",
)
@app_commands.describe(question="The question you want to ask.")
async def eight_ball(self, context: Context, *, question: str) -> None:
"""
Ask any question to the bot.
:param context: The hybrid command context.
:param question: The question that should be asked by the user.
"""
async def eight_ball(self, context, *, question: str):
answers = [
"It is certain.",
"It is decidedly so.",
@@ -48,10 +36,10 @@ class EightBall(commands.Cog, name="8ball"):
description=f"{random.choice(answers)}",
color=0x7289DA,
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
embed.set_footer(text=f"The question was: {question}")
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(EightBall(bot))
return eight_ball

View File

@@ -1,13 +1,15 @@
import random
from itertools import repeat
import discord
from discord.ext import commands
from discord.ext.commands import Context
import asyncio
import time
class RowButton(discord.ui.Button):
def __init__(self, ctx, label, custom_id, bombs, board):
super().__init__(label=label, style=discord.ButtonStyle.grey, custom_id=custom_id)
super().__init__(
label=label, style=discord.ButtonStyle.grey, custom_id=custom_id
)
self.ctx = ctx
self.bombs = bombs
self.board = board
@@ -15,7 +17,26 @@ class RowButton(discord.ui.Button):
async def callback(self, interaction):
assert self.view is not None
view: MsView = self.view
await interaction.response.defer()
current_time = time.time()
if current_time - view.last_interaction < 0.5:
try:
return await interaction.response.send_message(
"Please wait before clicking again.", ephemeral=True
)
except:
return
view.last_interaction = current_time
try:
await interaction.response.defer()
except discord.errors.HTTPException as e:
if e.status == 429:
await asyncio.sleep(1)
return
raise
if interaction.user.id != self.ctx.author.id:
return await interaction.followup.send(
"You cannot interact with these buttons.", ephemeral=True
@@ -23,7 +44,14 @@ class RowButton(discord.ui.Button):
b_id = self.custom_id
if int(b_id[5:]) in view.moves:
return await interaction.followup.send("That part is already taken.", ephemeral=True)
return await interaction.followup.send(
"That part is already taken.", ephemeral=True
)
if not view.bombs_generated:
view.generate_bombs(int(b_id[5:]))
self.bombs = view.bombs
if int(b_id[5:]) in self.bombs:
await view.RevealBombs(b_id, view.board)
else:
@@ -33,59 +61,94 @@ class RowButton(discord.ui.Button):
def checkpos(count, rawpos, pos):
pos = view.GetBoardPos(rawpos)
if not rawpos - 1 in self.bombs or pos == 0:
if rawpos - 1 not in self.bombs or pos == 0:
count.append(rawpos - 1)
if not rawpos + 1 in self.bombs or pos == 4:
if rawpos + 1 not in self.bombs or pos == 4:
count.append(rawpos + 1)
if not rawpos - 6 in self.bombs or pos == 0:
if rawpos - 6 not in self.bombs or pos == 0:
count.append(rawpos - 6)
if not rawpos - 4 in self.bombs or pos == 4:
if rawpos - 4 not in self.bombs or pos == 4:
count.append(rawpos - 4)
if not rawpos + 6 in self.bombs or pos == 4:
if rawpos + 6 not in self.bombs or pos == 4:
count.append(rawpos + 6)
if not rawpos + 4 in self.bombs or pos == 0:
if rawpos + 4 not in self.bombs or pos == 0:
count.append(rawpos + 4)
if not rawpos - 5 in self.bombs:
if rawpos - 5 not in self.bombs:
count.append(rawpos - 5)
if not rawpos + 5 in self.bombs:
if rawpos + 5 not in self.bombs:
count.append(rawpos + 5)
return count
count = checkpos(count, rawpos, pos)
number = 8-len(count)
number = 8 - len(count)
self.label = str(number) if number > 0 else "0"
self.style = discord.ButtonStyle.green
pos = int(b_id[5:])
view.board[view.GetBoardRow(pos)][
view.GetBoardPos(pos)
] = str(number) if number > 0 else "0"
view.board[view.GetBoardRow(pos)][view.GetBoardPos(pos)] = (
str(number) if number > 0 else "0"
)
view.moves.append(pos)
if len(view.moves) + len(self.bombs) == 25:
await interaction.edit_original_response(view=view)
await view.EndGame()
await interaction.edit_original_response(view=view)
else:
try:
await interaction.edit_original_response(view=view)
except discord.errors.HTTPException as e:
if e.status == 429:
await asyncio.sleep(1)
else:
raise
class MsView(discord.ui.View):
def __init__(self, ctx, options, bombs, board):
super().__init__()
def __init__(self, ctx, options, bomb_count, board):
super().__init__(timeout=300)
for i, op in enumerate(options):
self.add_item(RowButton(ctx, op, f"block{i}", bombs, board))
self.add_item(RowButton(ctx, op, f"block{i}", [], board))
self.board = board
self.bombs = bombs
self.bombs = []
self.bomb_count = bomb_count
self.bombs_generated = False
self.moves = []
self.ctx = ctx
self.message = None
self.last_interaction = 0
def generate_bombs(self, first_move_pos):
"""Generate bombs excluding the first clicked position"""
bombpositions = []
excluded_positions = [0, 4, 20, 24, first_move_pos]
while len(bombpositions) < self.bomb_count:
random_index = random.randint(0, 24)
if (
random_index not in bombpositions
and random_index not in excluded_positions
):
bombpositions.append(random_index)
self.bombs = bombpositions
self.bombs_generated = True
for button in self.children:
if isinstance(button, RowButton):
button.bombs = self.bombs
async def on_timeout(self):
for button in self.children:
button.disabled = True
embed = discord.Embed(
title="Minesweeper", description="Game timed out!", color=0xFF0000
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
try:
await self.message.edit(embed=embed, view=self)
except:
pass
async def EndGame(self):
embed = discord.Embed(
title="Minesweeper",
description="Game Ended. You won!",
color=0x00FF00
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
await self.message.edit(embed=embed, view=self)
for button in self.children:
button.disabled = True
pos = int(button.custom_id[5:])
@@ -94,6 +157,22 @@ class MsView(discord.ui.View):
button.style = discord.ButtonStyle.red
self.board[self.GetBoardRow(pos)][self.GetBoardPos(pos)] = "💣"
embed = discord.Embed(
title="Minesweeper", description="Game Ended. You won!", color=0x00FF00
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
try:
await self.message.edit(embed=embed, view=self)
except discord.errors.HTTPException as e:
if e.status == 429:
await asyncio.sleep(1)
else:
raise
self.stop()
def GetBoardRow(self, pos):
if pos in [0, 1, 2, 3, 4]:
return 0
@@ -130,14 +209,7 @@ class MsView(discord.ui.View):
async def RevealBombs(self, b_id, board):
bombemo = "💣"
embed = discord.Embed(
title="Minesweeper",
description=f"💥 BOOM! You hit a bomb. Game Over!\n-# gg {self.ctx.author.mention}",
color=0xE02B2B
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
await self.message.edit(embed=embed, view=self)
for button in self.children:
button.disabled = True
if button.custom_id == b_id:
@@ -151,34 +223,34 @@ class MsView(discord.ui.View):
button.label = bombemo
button.style = discord.ButtonStyle.red
pos = int(button.custom_id[5:])
self.board[self.GetBoardRow(pos)][
self.GetBoardPos(pos)
] = bombemo
self.board[self.GetBoardRow(pos)][self.GetBoardPos(pos)] = bombemo
embed = discord.Embed(
title="Minesweeper",
description=f"💥 BOOM! You hit a bomb. Game Over!\n-# gg {self.ctx.author.mention}",
color=0xE02B2B,
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
try:
await self.message.edit(embed=embed, view=self)
except discord.errors.HTTPException as e:
if e.status == 429:
await asyncio.sleep(1)
else:
raise
self.stop()
class Minesweeper(commands.Cog, name="minesweeper"):
def __init__(self, bot) -> None:
self.bot = bot
def minesweeper_command():
@commands.hybrid_command(
name="minesweeper",
description="Play a buttoned minesweeper mini-game."
name="minesweeper", description="Play a buttoned minesweeper mini-game."
)
async def minesweeper(self, context: Context) -> None:
"""
Play a buttoned minesweeper mini-game.
:param context: The hybrid command context.
"""
board = [[" "] * 5 for _ in range(5)] # Unicode block character, usually doesnt show up in Discord or github, search up invisible character on google
bombs = 0
bombpositions = []
for x in repeat(None, random.randint(4, 11)):
random_index = random.randint(0, 24)
if random_index not in bombpositions and random_index not in [
0, 4, 20, 24
]:
bombpositions.append(random_index)
bombs += 1
async def minesweeper(self, context):
board = [[" "] * 5 for _ in range(5)]
bomb_count = random.randint(4, 11)
def ExtractBlocks():
new_b = []
@@ -189,15 +261,15 @@ class Minesweeper(commands.Cog, name="minesweeper"):
embed = discord.Embed(
title="Minesweeper",
description=f"💣 Total Bombs: `{len(bombpositions)}`\n\nClick the buttons to reveal the grid. Avoid the bombs!",
color=0x7289DA
description=f"💣 Total Bombs: `{bomb_count}`\n\nClick the buttons to reveal the grid. Avoid the bombs!",
color=0x7289DA,
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
view = MsView(context, ExtractBlocks(), bombpositions, board)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
view = MsView(context, ExtractBlocks(), bomb_count, board)
message = await context.send(embed=embed, view=view)
view.message = message
async def setup(bot) -> None:
await bot.add_cog(Minesweeper(bot))
return minesweeper

View File

@@ -1,36 +1,18 @@
import aiohttp
import discord
from discord.ext import commands
from discord.ext.commands import Context
class RandomFact(commands.Cog, name="randomfact"):
def __init__(self, bot) -> None:
self.bot = bot
def randomfact_command():
@commands.hybrid_command(name="randomfact", description="Get a random fact.")
async def randomfact(self, context: Context) -> None:
async with aiohttp.ClientSession() as session:
async with session.get(
"https://uselessfacts.jsph.pl/random.json?language=en"
) as request:
if request.status == 200:
data = await request.json()
embed = discord.Embed(
title="Random Fact",
description=data["text"],
color=0x7289DA
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
else:
embed = discord.Embed(
title="Error!",
description="There is something wrong with the API, please try again later",
color=0xE02B2B,
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
await context.send(embed=embed)
async def randomfact(self, context):
embed = discord.Embed(
title="Command Disabled",
description="This command is currently disabled.",
color=0xE02B2B,
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(RandomFact(bot))
return randomfact

View File

@@ -1,7 +1,7 @@
import random
import discord
from discord.ext import commands
from discord.ext.commands import Context
class RockPaperScissors(discord.ui.Select):
def __init__(self) -> None:
@@ -35,13 +35,14 @@ class RockPaperScissors(discord.ui.Select):
bot_choice_index = choices[bot_choice]
result_embed = discord.Embed(title="Rock Paper Scissors", color=0x7289DA)
result_embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
result_embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
winner = (3 + user_choice_index - bot_choice_index) % 3
# Get the user mention
user_mention = interaction.user.mention
if winner == 0:
result_embed.description = f"**That's a draw!** You've chosen {user_choice} and I've chosen {bot_choice}.\n-# gg {user_mention}"
result_embed.colour = 0xF59E42
@@ -56,27 +57,27 @@ class RockPaperScissors(discord.ui.Select):
embed=result_embed, content=None, view=None
)
class RockPaperScissorsView(discord.ui.View):
def __init__(self) -> None:
super().__init__()
self.add_item(RockPaperScissors())
class RPS(commands.Cog, name="rps"):
def __init__(self, bot) -> None:
self.bot = bot
def rps_command():
@commands.hybrid_command(
name="rps", description="Play the rock paper scissors game against the bot."
)
async def rock_paper_scissors(self, context: Context) -> None:
async def rock_paper_scissors(self, context):
view = RockPaperScissorsView()
embed = discord.Embed(
title="Rock Paper Scissors",
description="Please make your choice",
color=0x7289DA
color=0x7289DA,
)
embed.set_author(
name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp"
)
embed.set_author(name="Fun", icon_url="https://yes.nighty.works/raw/eW5lLm.webp")
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(RPS(bot))
return rock_paper_scissors

128
cogs/general/__init__.py Normal file
View File

@@ -0,0 +1,128 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .ping import ping_command
from .uptime import uptime_command
from .serverinfo import serverinfo_command
from .feedback import feedback_command
from .userinfo import userinfo_command
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class General(commands.GroupCog, name="general"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="general", invoke_without_command=True)
async def general_group(self, context: Context):
embed = discord.Embed(
title="General Commands",
description="Use `.general <subcommand>` or `/general <subcommand>`.",
color=0x7289DA,
)
embed.set_author(
name="General", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
embed.add_field(
name="Available",
value="ping, uptime, serverinfo, userinfo, feedback",
inline=False,
)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str, **kwargs):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command, **kwargs)
else:
await context.send(f"Unknown general command: {name}")
@general_group.command(name="ping")
async def general_group_ping(self, context: Context):
await self._invoke_hybrid(context, "ping")
@general_group.command(name="uptime")
async def general_group_uptime(self, context: Context):
await self._invoke_hybrid(context, "uptime")
@general_group.command(name="serverinfo")
async def general_group_serverinfo(self, context: Context):
await self._invoke_hybrid(context, "serverinfo")
@general_group.command(name="userinfo")
async def general_group_userinfo(
self, context: Context, user: discord.User = None, user_id: str = None
):
await self._invoke_hybrid(context, "userinfo", user=user, user_id=user_id)
@general_group.command(name="feedback")
async def general_group_feedback(self, context: Context):
await self._invoke_hybrid(context, "feedback")
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="ping",
description="Check if the bot is alive.",
)
async def ping(self, context):
return await ping_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="uptime",
description="Check how long the bot has been running.",
)
async def uptime(self, context):
return await uptime_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="serverinfo",
description="Get some useful (or not) information about the server.",
)
async def serverinfo(self, context):
return await serverinfo_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="userinfo",
description="Get information on a user.",
)
@app_commands.describe(
user="User to get info for", user_id="User ID to get info for"
)
async def userinfo(self, context, user: discord.User = None, user_id: str = None):
return await userinfo_command()(self, context, user=user, user_id=user_id)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="feedback", description="Submit a feedback for the owners of the bot"
)
async def feedback(self, context):
return await feedback_command()(self, context)
async def setup(bot) -> None:
cog = General(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'general.ping'")
bot.logger.info("Loaded extension 'general.uptime'")
bot.logger.info("Loaded extension 'general.serverinfo'")
bot.logger.info("Loaded extension 'general.userinfo'")
bot.logger.info("Loaded extension 'general.feedback'")

View File

@@ -1,41 +0,0 @@
import platform
import discord
from discord.ext import commands
from discord.ext.commands import Context
class BotInfo(commands.Cog, name="botinfo"):
def __init__(self, bot) -> None:
self.bot = bot
@commands.hybrid_command(
name="botinfo",
description="Get some useful (or not) information about the bot.",
)
async def botinfo(self, context: Context) -> None:
"""
Get some useful (or not) information about the bot.
:param context: The hybrid command context.
"""
embed = discord.Embed(
title="Syntrel Discord Bot",
color=0x7289DA,
)
embed.set_author(name="Bot Information", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp")
embed.add_field(name="Owner:", value="[neoarz](https://discordapp.com/users/1015372540937502851)", inline=True)
embed.add_field(
name="Python Version:", value=f"{platform.python_version()}", inline=True
)
embed.add_field(
name="Prefix:",
value=f"/ (Slash Commands) or {self.bot.bot_prefix} for normal commands",
inline=False,
)
if getattr(context, "interaction", None):
await context.interaction.response.send_message(embed=embed, ephemeral=True)
else:
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(BotInfo(bot))

View File

@@ -1,9 +1,8 @@
import discord
from discord import app_commands
from discord.ext import commands
class FeedbackForm(discord.ui.Modal, title="Feeedback"):
class FeedbackForm(discord.ui.Modal, title="Feedback"):
feedback = discord.ui.TextInput(
label="What do you think about this bot?",
style=discord.TextStyle.long,
@@ -18,42 +17,51 @@ class FeedbackForm(discord.ui.Modal, title="Feeedback"):
self.stop()
class Feedback(commands.Cog, name="feedback"):
def __init__(self, bot) -> None:
self.bot = bot
@app_commands.command(
def feedback_command():
@commands.hybrid_command(
name="feedback", description="Submit a feedback for the owners of the bot"
)
async def feedback(self, interaction: discord.Interaction) -> None:
"""
Submit a feedback for the owners of the bot.
async def feedback(self, context):
if getattr(context, "interaction", None):
interaction = context.interaction
feedback_form = FeedbackForm()
await interaction.response.send_modal(feedback_form)
:param interaction: The application command interaction.
"""
feedback_form = FeedbackForm()
await interaction.response.send_modal(feedback_form)
await feedback_form.wait()
interaction = feedback_form.interaction
await interaction.response.send_message(
embed=discord.Embed(
title="Thank You!",
description="Your feedback has been submitted, the owners have been notified about it.",
color=0x7289DA,
).set_author(
name="Feedback System",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
),
ephemeral=True,
)
await feedback_form.wait()
interaction = feedback_form.interaction
await interaction.response.send_message(
embed=discord.Embed(
title="Thank You!",
description="Your feedback has been submitted, the owners have been notified about it.",
color=0x7289DA,
).set_author(name="Feedback System", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"),
ephemeral=True,
)
app_owner = (await self.bot.application_info()).owner
await app_owner.send(
embed=discord.Embed(
title="New Feedback",
description=f"{interaction.user} (<@{interaction.user.id}>) has submitted a new feedback:\n```\n{feedback_form.answer}\n```",
color=0x7289DA,
).set_author(
name="Feedback System",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
)
)
else:
embed = discord.Embed(
title="Error!",
description="This command can only be used as a slash command. Please use `/general feedback` instead.",
color=0xE02B2B,
)
embed.set_author(
name="Feedback System",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
)
await context.send(embed=embed)
app_owner = (await self.bot.application_info()).owner
await app_owner.send(
embed=discord.Embed(
title="New Feedback",
description=f"{interaction.user} (<@{interaction.user.id}>) has submitted a new feedback:\n```\n{feedback_form.answer}\n```",
color=0x7289DA,
).set_author(name="Feedback System", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp")
)
async def setup(bot) -> None:
await bot.add_cog(Feedback(bot))
return feedback

View File

@@ -1,190 +0,0 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Help(commands.Cog, name="help"):
def __init__(self, bot) -> None:
self.bot = bot
async def category_autocomplete(
self,
interaction: discord.Interaction,
current: str,
) -> list[app_commands.Choice[str]]:
categories = ["general", "fun", "moderation", "template", "owner", "sidestore"]
suggestions = []
for category in categories:
if current.lower() in category.lower():
suggestions.append(
app_commands.Choice(
name=f"{category.capitalize()} Commands",
value=category
)
)
return suggestions[:25]
@commands.hybrid_command(
name="help", description="List all commands the bot has loaded."
)
@app_commands.describe(category="Choose a specific category to view its commands")
@app_commands.autocomplete(category=category_autocomplete)
async def help(self, context: Context, category: str = None) -> None:
category_mapping = {
"help": "general",
"botinfo": "general",
"serverinfo": "general",
"ping": "general",
"feedback": "general",
# "context_menus": "general",
"randomfact": "fun",
"coinflip": "fun",
"rps": "fun",
"8ball": "fun",
"minesweeper": "fun",
"kick": "moderation",
"ban": "moderation",
"nick": "moderation",
"purge": "moderation",
"hackban": "moderation",
"warnings": "moderation",
"archive": "moderation",
"sidestore": "sidestore",
"refresh": "sidestore",
"code": "sidestore",
"crash": "sidestore",
"pairing": "sidestore",
"server": "sidestore",
"half": "sidestore",
"sparse": "sidestore",
"sync": "owner",
"cog_management": "owner",
"shutdown": "owner",
"say": "owner",
"invite": "owner",
}
category_descriptions = {
"general": "General commands",
"fun": "Funny commands",
"moderation": "Administration commands",
"template": "Template commands",
"owner": "Owner commands",
"sidestore": "SideStore troubleshooting commands"
}
if category is None:
embed = discord.Embed(
title="Help",
color=0x7289DA
)
embed.set_author(name="Help", icon_url="https://yes.nighty.works/raw/T9mnBO.png")
available_categories = set()
for cog_name in self.bot.cogs:
mapped_category = category_mapping.get(cog_name.lower())
if mapped_category:
available_categories.add(mapped_category)
category_list = []
for cat in sorted(available_categories):
description = category_descriptions.get(cat, f"{cat.capitalize()} commands")
category_list.append(f"**/help {cat}** » {description}")
if category_list:
embed.add_field(
name="",
value="\n".join(category_list),
inline=False
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, ephemeral=True)
else:
await context.author.send(embed=embed)
return
category = category.lower()
if category not in category_descriptions:
embed = discord.Embed(
title="Error",
description=f"Category '{category}' not found. Use `/help` to see available categories.",
color=0x7289DA
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, ephemeral=True)
else:
await context.author.send(embed=embed)
return
commands_in_category = []
seen_names = set()
for cog_name in self.bot.cogs:
if category_mapping.get(cog_name.lower()) == category:
cog = self.bot.get_cog(cog_name)
if cog:
commands_list = cog.get_commands()
for command in commands_list:
name = command.name
if name in seen_names:
continue
description = command.description.partition("\n")[0] if command.description else "No description available"
commands_in_category.append((name, description))
seen_names.add(name)
for app_command in self.bot.tree.get_commands():
bound_cog = getattr(app_command, "binding", None)
if bound_cog is None:
continue
bound_cog_name = getattr(bound_cog, "qualified_name", "").lower()
if category_mapping.get(bound_cog_name) != category:
continue
if app_command.name in seen_names:
continue
description = app_command.description.partition("\n")[0] if getattr(app_command, "description", None) else "No description available"
commands_in_category.append((app_command.name, description))
seen_names.add(app_command.name)
if not commands_in_category:
embed = discord.Embed(
title="Error",
description=f"No commands found in category '{category}'.",
color=0x7289DA
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, ephemeral=True)
else:
await context.author.send(embed=embed)
return
embed = discord.Embed(
title=f"/help » {category.lower()}",
color=0x7289DA
)
embed.set_author(name="Help", icon_url="https://yes.nighty.works/raw/T9mnBO.png")
data = []
for command_name, description in sorted(commands_in_category):
data.append(f"**/{command_name}** » {description}")
help_text = "\n".join(data)
embed.add_field(
name="",
value=help_text,
inline=False
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, ephemeral=True)
else:
await context.author.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(Help(bot))

View File

@@ -1,41 +1,28 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import discord
from discord.ext import commands
from discord.ext.commands import Context
class Ping(commands.Cog, name="ping"):
def __init__(self, bot) -> None:
self.bot = bot
def ping_command():
@commands.hybrid_command(
name="ping",
description="Check if the bot is alive.",
)
async def ping(self, context: Context) -> None:
"""
Check if the bot is alive.
:param context: The hybrid command context.
"""
async def ping(self, context):
embed = discord.Embed(
title="🏓 Pong!",
description=f"The bot latency is {round(self.bot.latency * 1000)}ms.",
color=0x7289DA,
)
embed.set_author(name="Ping", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp")
embed.set_author(
name="Ping", icon_url="https://yes.nighty.works/raw/gSxqzV.png"
)
if getattr(context, "interaction", None):
await context.interaction.response.send_message(embed=embed, ephemeral=True)
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(embed=embed, ephemeral=False)
else:
await inter.followup.send(embed=embed, ephemeral=False)
else:
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(Ping(bot))
return ping

View File

@@ -1,58 +1,164 @@
import discord
from discord.ext import commands
from discord.ext.commands import Context
class ServerInfo(commands.Cog, name="serverinfo"):
def __init__(self, bot) -> None:
self.bot = bot
def serverinfo_command():
@commands.hybrid_command(
name="serverinfo",
description="Get some useful (or not) information about the server.",
)
@commands.guild_only() # This decorator ensures the command only works in servers
async def serverinfo(self, context: Context) -> None:
"""
Get some useful (or not) information about the server.
:param context: The hybrid command context.
"""
# Additional check (though @commands.guild_only() should handle this)
@commands.guild_only()
async def serverinfo(self, context):
if context.guild is None:
await context.send("This command can only be used in a server, not in DMs!")
return
roles = [role.name for role in context.guild.roles]
num_roles = len(roles)
if num_roles > 50:
roles = roles[:50]
roles.append(f">>>> Displaying [50/{num_roles}] Roles")
roles = ", ".join(roles)
guild = context.guild
text_channels = len(
[c for c in guild.channels if isinstance(c, discord.TextChannel)]
)
voice_channels = len(
[c for c in guild.channels if isinstance(c, discord.VoiceChannel)]
)
category_channels = len(
[c for c in guild.channels if isinstance(c, discord.CategoryChannel)]
)
forum_channels = len(
[c for c in guild.channels if isinstance(c, discord.ForumChannel)]
)
stage_channels = len(
[c for c in guild.channels if isinstance(c, discord.StageChannel)]
)
age_restricted = len(
[c for c in guild.channels if hasattr(c, "nsfw") and c.nsfw]
)
hidden_channels = len(
[
c
for c in guild.channels
if c.permissions_for(guild.default_role).view_channel == False
]
)
managed_roles = len([r for r in guild.roles if r.managed])
animated_emojis = len([e for e in guild.emojis if e.animated])
managed_emojis = len([e for e in guild.emojis if e.managed])
unavailable_emojis = len([e for e in guild.emojis if not e.available])
png_stickers = len(
[s for s in guild.stickers if s.format == discord.StickerFormatType.png]
)
apng_stickers = len(
[s for s in guild.stickers if s.format == discord.StickerFormatType.apng]
)
gif_stickers = len(
[s for s in guild.stickers if s.format == discord.StickerFormatType.lottie]
)
lottie_stickers = len(
[s for s in guild.stickers if s.format == discord.StickerFormatType.lottie]
)
online_members = len(
[m for m in guild.members if m.status == discord.Status.online]
)
idle_members = len(
[m for m in guild.members if m.status == discord.Status.idle]
)
dnd_members = len([m for m in guild.members if m.status == discord.Status.dnd])
offline_members = len(
[m for m in guild.members if m.status == discord.Status.offline]
)
bot_count = len([m for m in guild.members if m.bot])
human_count = guild.member_count - bot_count
created_delta = discord.utils.utcnow() - guild.created_at
years_ago = created_delta.days // 365
embed = discord.Embed(
title="**Server Name:**",
description=f"{context.guild}",
color=0x7289DA
).set_author(name="Server Information", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp")
if context.guild.icon is not None:
embed.set_thumbnail(url=context.guild.icon.url)
embed.add_field(name="Server ID", value=context.guild.id)
embed.add_field(name="Member Count", value=context.guild.member_count)
embed.add_field(
name="Text/Voice Channels",
value=f"{len(context.guild.channels)}"
title=f"**Server Name:** {guild.name}", color=0x7289DA
).set_author(
name="Server Information",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
)
embed.add_field(
name=f"Roles ({len(context.guild.roles)})",
value=roles
if guild.icon is not None:
embed.set_thumbnail(url=guild.icon.url)
owner_value = (
guild.owner.mention
if guild.owner
else (f"<@{guild.owner_id}>" if guild.owner_id else "Unknown")
)
embed.set_footer(text=f"Created at: {context.guild.created_at.strftime('%m/%d/%Y')}")
embed.add_field(name="Owner", value=owner_value, inline=True)
embed.add_field(
name="Created",
value=f"{years_ago} year{'s' if years_ago != 1 else ''} ago",
inline=True,
)
embed.add_field(
name="Max Members",
value=f"{guild.max_members:,}" if guild.max_members else "Unknown",
inline=True,
)
boost_level = guild.premium_tier
boost_count = guild.premium_subscription_count or 0
embed.add_field(
name="Boost Status",
value=f"Level {boost_level}, {boost_count} Boost{'s' if boost_count != 1 else ''}",
inline=False,
)
channels_info = f"{text_channels} text, {voice_channels} voice, {category_channels} category"
if forum_channels > 0:
channels_info += f", {forum_channels} forum"
if stage_channels > 0:
channels_info += f", {stage_channels} stage"
channels_info += f"\n{age_restricted} age restricted, {hidden_channels} hidden"
embed.add_field(
name=f"Channels ({len(guild.channels)})", value=channels_info, inline=True
)
roles_info = f"{len(guild.roles)} total\n{managed_roles} managed"
embed.add_field(
name=f"Roles ({len(guild.roles)})", value=roles_info, inline=True
)
emotes_info = f"{len(guild.emojis)} total\n{animated_emojis} animated, {managed_emojis} managed"
if unavailable_emojis > 0:
emotes_info += f"\n{unavailable_emojis} unavailable"
embed.add_field(
name=f"Emotes ({len(guild.emojis)})", value=emotes_info, inline=True
)
if len(guild.stickers) > 0:
stickers_info = f"{len(guild.stickers)} total\n{png_stickers} PNG, {apng_stickers} APNG, {gif_stickers} GIF, {lottie_stickers} Lottie"
embed.add_field(
name=f"Stickers ({len(guild.stickers)})",
value=stickers_info,
inline=True,
)
embed.add_field(
name="Member Count", value=f"{guild.member_count}", inline=False
)
embed.set_footer(
text=f"Server ID: {guild.id} • Created: {guild.created_at.strftime('%m/%d/%Y')}"
)
if getattr(context, "interaction", None):
await context.interaction.response.send_message(embed=embed, ephemeral=True)
await context.interaction.response.send_message(
embed=embed, ephemeral=False
)
else:
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(ServerInfo(bot))
return serverinfo

53
cogs/general/uptime.py Normal file
View File

@@ -0,0 +1,53 @@
import discord
from discord.ext import commands
class UptimeView(discord.ui.View):
def __init__(self, bot):
super().__init__(timeout=300)
self.bot = bot
@discord.ui.button(
emoji="<:RefreshEmoji:1418934990770802891>", style=discord.ButtonStyle.primary
)
async def refresh_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
embed = discord.Embed(
title="Bot Uptime",
description=f"The bot has been running for {self.bot.get_uptime()}",
color=0x7289DA,
)
embed.set_author(
name="Uptime", icon_url="https://yes.nighty.works/raw/gSxqzV.png"
)
await interaction.response.edit_message(embed=embed, view=self)
def uptime_command():
@commands.hybrid_command(
name="uptime",
description="Check how long the bot has been running.",
)
async def uptime(self, context):
embed = discord.Embed(
title="Bot Uptime",
description=f"The bot has been running for **{self.bot.get_uptime()}**",
color=0x7289DA,
)
embed.set_author(
name="Uptime", icon_url="https://yes.nighty.works/raw/gSxqzV.png"
)
view = UptimeView(self.bot)
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(
embed=embed, view=view, ephemeral=True
)
else:
await inter.followup.send(embed=embed, view=view, ephemeral=True)
else:
await context.send(embed=embed, view=view)
return uptime

621
cogs/general/userinfo.py Normal file
View File

@@ -0,0 +1,621 @@
import discord
from discord.ext import commands
from discord import app_commands
import aiohttp
from io import BytesIO
from PIL import Image
from datetime import datetime, timezone
ONE_MONTH = 2628000
quests_fetch = 0
quest_data = []
ACTIVITY_TYPE_NAMES = [
"Playing",
"Streaming",
"Listening to",
"Watching",
"Custom Status",
"Competing in",
"Hang Status",
]
USER_FLAGS = {
"STAFF": 1 << 0,
"PARTNER": 1 << 1,
"HYPESQUAD": 1 << 2,
"BUG_HUNTER_LEVEL_1": 1 << 3,
"HYPESQUAD_ONLINE_HOUSE_1": 1 << 6,
"HYPESQUAD_ONLINE_HOUSE_2": 1 << 7,
"HYPESQUAD_ONLINE_HOUSE_3": 1 << 8,
"PREMIUM_EARLY_SUPPORTER": 1 << 9,
"BUG_HUNTER_LEVEL_2": 1 << 14,
"VERIFIED_DEVELOPER": 1 << 17,
"CERTIFIED_MODERATOR": 1 << 18,
"ACTIVE_DEVELOPER": 1 << 22,
}
APPLICATION_FLAGS = {
"APPLICATION_COMMAND_BADGE": 1 << 23,
"AUTO_MODERATION_RULE_CREATE_BADGE": 1 << 6,
}
BADGE_URLS = {
"staff": "https://discord.com/company",
"partner": "https://discord.com/partners",
"certified_moderator": "https://discord.com/safety",
"hypesquad": "https://discord.com/hypesquad",
"hypesquad_house_1": "https://discord.com/settings/hypesquad-online",
"hypesquad_house_2": "https://discord.com/settings/hypesquad-online",
"hypesquad_house_3": "https://discord.com/settings/hypesquad-online",
"bug_hunter_level_1": "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs",
"bug_hunter_level_2": "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs",
"active_developer": "https://support-dev.discord.com/hc/en-us/articles/10113997751447?ref=badge",
"early_supporter": "https://discord.com/settings/premium",
"premium": "https://discord.com/settings/premium",
"bot_commands": "https://discord.com/blog/welcome-to-the-new-era-of-discord-apps?ref=badge",
"quest_completed": "https://discord.com/settings/inventory",
}
BADGE_ICONS = {
"staff": "<:discordstaff:1426051878155845702>",
"partner": "<:discordpartner:1426051933608873986>",
"certified_moderator": "<:discordmod:1426051921826943050>",
"hypesquad": "<:hypesquadevents:1426051833536970852>",
"hypesquad_house_1": "<:hypesquadbravery:1426051916739383297>",
"hypesquad_house_2": "<:hypesquadbrilliance:1426051849433387068>",
"hypesquad_house_3": "<:hypesquadbalance:1426051905179750495>",
"bug_hunter_level_1": "<:discordbughunter1:1426052002193997895>",
"bug_hunter_level_2": "<:discordbughunter2:1426052028257406987>",
"active_developer": "<:activedeveloper:1426051981658685552>",
"verified_developer": "<:discordbotdev:1426051827077480570>",
"early_supporter": "<:discordearlysupporter:1426052023165517924>",
"premium": "<:discordnitro:1426051911123206296>",
"guild_booster_lvl1": "<:discordboost1:1426052007294144605>",
"guild_booster_lvl2": "<:discordboost2:1426051986985582692>",
"guild_booster_lvl3": "<:discordboost3:1426051991812964434>",
"guild_booster_lvl4": "<:discordboost4:1426051955473645671>",
"guild_booster_lvl5": "<:discordboost5:1426051960456609824>",
"guild_booster_lvl6": "<:discordboost6:1426051976583712918>",
"guild_booster_lvl7": "<:discordboost7:1426051965808410634>",
"guild_booster_lvl8": "<:discordboost8:1426051844014342225>",
"guild_booster_lvl9": "<:discordboost9:1426051855015743558>",
"bot_commands": "<:supportscommands:1426051872476889171>",
"automod": "<:automod:1426051939103146115>",
"quest_completed": "<:quest:1426051817946611784>",
"username": "<:username:1426051894371160115>",
"premium_bot": "<:premiumbot:1426051888272638025>",
"orb": "<:orb:1426051861605126289>",
"bronze": "<:bronze:1426051866969772034>",
"silver": "<:silver:1426051928575709286>",
"gold": "<:gold:1426052012352737333>",
"platinum": "<:platinum:1426052018040082545>",
"diamond": "<:diamond:1426051944685895771>",
"emerald": "<:emerald:1426051812313792537>",
"ruby": "<:ruby:1426051838645637150>",
"opal": "<:opal:1426051883247603762>",
}
ACTIVITY_TYPE_ICONS = {
0: "<:gaming:1426409065701048451>",
2: "<:music:1426409047132737586>",
3: "<:watching:1426409475450863778>",
}
def format_username(user):
if user.discriminator and user.discriminator != "0":
return f"{user.name}#{user.discriminator}"
return f"@{user.name}"
def get_default_avatar(user_id, discriminator=None):
if discriminator and int(discriminator) > 0:
index = int(discriminator) % 5
else:
index = (int(user_id) >> 22) % 6
return f"https://cdn.discordapp.com/embed/avatars/{index}.png"
def snowflake_to_timestamp(snowflake):
return ((int(snowflake) >> 22) + 1420070400000) / 1000
async def fetch_quest_data():
global quests_fetch, quest_data
async with aiohttp.ClientSession() as session:
async with session.get(
"https://raw.githubusercontent.com/aamiaa/discord-api-diff/refs/heads/main/quests.json"
) as resp:
quest_data = await resp.json()
quests_fetch = int(datetime.now().timestamp() * 1000) + 3600000
async def get_user_data(bot, user_id):
headers = {"Authorization": f"Bot {bot.http.token}"}
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://discord.com/api/v10/users/{user_id}", headers=headers
) as resp:
if resp.status == 404:
return None
return await resp.json()
async def get_application_data(bot, app_id):
headers = {"Authorization": f"Bot {bot.http.token}"}
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://discord.com/api/v10/applications/{app_id}/rpc", headers=headers
) as resp:
if resp.status in [404, 403, 10002]:
return None
return await resp.json()
async def get_published_listing(bot, sku_id):
headers = {"Authorization": f"Bot {bot.http.token}"}
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://discord.com/api/v10/store/published-listings/skus/{sku_id}",
headers=headers,
) as resp:
if resp.status != 200:
return None
return await resp.json()
def userinfo_command():
@commands.hybrid_command(
name="userinfo",
description="Get information on a user.",
)
@app_commands.describe(
user="User to get info for", user_id="User ID to get info for"
)
async def userinfo(self, context, user: discord.User = None, user_id: str = None):
await context.defer()
bot = self.bot
target_user = user if user else context.author
if user_id:
try:
target_user = await bot.fetch_user(int(user_id))
except:
await context.send("User not found.")
return
user_data = await get_user_data(bot, target_user.id)
if not user_data:
await context.send("Failed to fetch user data.")
return
guild = context.guild
member = guild.get_member(target_user.id) if guild else None
if int(datetime.now().timestamp() * 1000) > quests_fetch:
try:
await fetch_quest_data()
except:
pass
badges = []
flags = user_data.get("public_flags", 0)
if str(target_user.id) == "1015372540937502851":
badges.append(f"[{BADGE_ICONS['staff']}]({BADGE_URLS['staff']})")
badges.append(f"[{BADGE_ICONS['partner']}]({BADGE_URLS['partner']})")
badges.append(
f"[{BADGE_ICONS['certified_moderator']}]({BADGE_URLS['certified_moderator']})"
)
badges.append(
f"[{BADGE_ICONS['hypesquad_house_1']}]({BADGE_URLS['hypesquad_house_1']})"
)
badges.append(
f"[{BADGE_ICONS['bug_hunter_level_2']}]({BADGE_URLS['bug_hunter_level_2']})"
)
badges.append(BADGE_ICONS["verified_developer"])
badges.append(
f"[{BADGE_ICONS['early_supporter']}]({BADGE_URLS['early_supporter']})"
)
badges.append(
f"[{BADGE_ICONS['guild_booster_lvl9']}]({BADGE_URLS['premium']})"
)
badges.append(
f"[{BADGE_ICONS['quest_completed']}]({BADGE_URLS['quest_completed']})"
)
badges.append(BADGE_ICONS["username"])
elif str(target_user.id) == "1376728824108286034":
badges.append(BADGE_ICONS["automod"])
badges.append(BADGE_ICONS["verified_developer"])
badges.append(BADGE_ICONS["premium_bot"])
elif flags & USER_FLAGS["STAFF"]:
badges.append(f"[{BADGE_ICONS['staff']}]({BADGE_URLS['staff']})")
if flags & USER_FLAGS["PARTNER"]:
badges.append(f"[{BADGE_ICONS['partner']}]({BADGE_URLS['partner']})")
if flags & USER_FLAGS["CERTIFIED_MODERATOR"]:
badges.append(
f"[{BADGE_ICONS['certified_moderator']}]({BADGE_URLS['certified_moderator']})"
)
if flags & USER_FLAGS["HYPESQUAD"]:
badges.append(f"[{BADGE_ICONS['hypesquad']}]({BADGE_URLS['hypesquad']})")
if flags & USER_FLAGS["HYPESQUAD_ONLINE_HOUSE_1"]:
badges.append(
f"[{BADGE_ICONS['hypesquad_house_1']}]({BADGE_URLS['hypesquad_house_1']})"
)
if flags & USER_FLAGS["HYPESQUAD_ONLINE_HOUSE_2"]:
badges.append(
f"[{BADGE_ICONS['hypesquad_house_2']}]({BADGE_URLS['hypesquad_house_2']})"
)
if flags & USER_FLAGS["HYPESQUAD_ONLINE_HOUSE_3"]:
badges.append(
f"[{BADGE_ICONS['hypesquad_house_3']}]({BADGE_URLS['hypesquad_house_3']})"
)
if flags & USER_FLAGS["BUG_HUNTER_LEVEL_1"]:
badges.append(
f"[{BADGE_ICONS['bug_hunter_level_1']}]({BADGE_URLS['bug_hunter_level_1']})"
)
if flags & USER_FLAGS["BUG_HUNTER_LEVEL_2"]:
badges.append(
f"[{BADGE_ICONS['bug_hunter_level_2']}]({BADGE_URLS['bug_hunter_level_2']})"
)
if flags & USER_FLAGS["ACTIVE_DEVELOPER"]:
badges.append(
f"[{BADGE_ICONS['active_developer']}]({BADGE_URLS['active_developer']})"
)
if flags & USER_FLAGS["VERIFIED_DEVELOPER"]:
badges.append(BADGE_ICONS["verified_developer"])
if flags & USER_FLAGS["PREMIUM_EARLY_SUPPORTER"]:
badges.append(
f"[{BADGE_ICONS['early_supporter']}]({BADGE_URLS['early_supporter']})"
)
avatar_hash = user_data.get("avatar", "")
banner_hash = user_data.get("banner")
if (
banner_hash or (avatar_hash and avatar_hash.startswith("a_"))
) and not user_data.get("bot"):
badges.append(f"[{BADGE_ICONS['premium']}]({BADGE_URLS['premium']})")
if member and member.premium_since:
boosting_since = member.premium_since
delta = (datetime.now(timezone.utc) - boosting_since).total_seconds()
icon = BADGE_ICONS["guild_booster_lvl1"]
if delta >= ONE_MONTH * 24:
icon = BADGE_ICONS["guild_booster_lvl9"]
elif delta >= ONE_MONTH * 18:
icon = BADGE_ICONS["guild_booster_lvl8"]
elif delta >= ONE_MONTH * 15:
icon = BADGE_ICONS["guild_booster_lvl7"]
elif delta >= ONE_MONTH * 12:
icon = BADGE_ICONS["guild_booster_lvl6"]
elif delta >= ONE_MONTH * 9:
icon = BADGE_ICONS["guild_booster_lvl5"]
elif delta >= ONE_MONTH * 6:
icon = BADGE_ICONS["guild_booster_lvl4"]
elif delta >= ONE_MONTH * 3:
icon = BADGE_ICONS["guild_booster_lvl3"]
elif delta >= ONE_MONTH * 2:
icon = BADGE_ICONS["guild_booster_lvl2"]
badges.append(f"[{icon}]({BADGE_URLS['premium']})")
bot_deleted = False
if user_data.get("bot"):
app_data = await get_application_data(bot, target_user.id)
if app_data:
app_flags = app_data.get("flags", 0)
if app_flags & APPLICATION_FLAGS["APPLICATION_COMMAND_BADGE"]:
badges.append(
f"[{BADGE_ICONS['bot_commands']}]({BADGE_URLS['bot_commands']})"
)
if app_flags & APPLICATION_FLAGS["AUTO_MODERATION_RULE_CREATE_BADGE"]:
badges.append(BADGE_ICONS["automod"])
else:
bot_deleted = True
if user_data.get("system"):
bot_deleted = False
quest_decoration_name = None
avatar_decoration = user_data.get("avatar_decoration_data")
if avatar_decoration and avatar_decoration.get("sku_id"):
for quest in quest_data:
config = quest.get("config", {})
rewards = config.get("rewards_config", {}).get(
"rewards", []
) or config.get("rewards", [])
for reward in rewards:
if (
reward.get("type") == 3
and reward.get("sku_id") == avatar_decoration["sku_id"]
):
quest_decoration_name = (
reward.get("name")
or reward.get("messages", {}).get("name")
or "*Unknown*"
).replace("Avatar Decoration", "Avatar Deco")
badges.append(
f"[{BADGE_ICONS['quest_completed']}]({BADGE_URLS['quest_completed']})"
)
break
if quest_decoration_name:
break
elif avatar_decoration and (
avatar_decoration.get("expires_at")
or avatar_decoration.get("sku_id") == "1226939756617793606"
):
badges.append(
f"[{BADGE_ICONS['quest_completed']}]({BADGE_URLS['quest_completed']})"
)
if user_data.get("legacy_username"):
badges.append(BADGE_ICONS["username"])
if user_data.get("bot") and user_data.get("approximated_guild_count"):
badges.append(BADGE_ICONS["premium_bot"])
profile_effect = user_data.get("profile_effect")
if profile_effect:
effect_id = profile_effect.get("id")
if effect_id:
orb_tier = None
if "1139323098643333240" in effect_id:
orb_tier = "opal"
elif "1139323095841308733" in effect_id:
orb_tier = "ruby"
elif "1139323090842013756" in effect_id:
orb_tier = "emerald"
elif "1139323087608832090" in effect_id:
orb_tier = "diamond"
elif "1144286544523669516" in effect_id:
orb_tier = "platinum"
elif "1139323084127289374" in effect_id:
orb_tier = "gold"
elif "1139323078435717220" in effect_id:
orb_tier = "silver"
elif "1139323075214307448" in effect_id:
orb_tier = "bronze"
else:
orb_tier = "orb"
if orb_tier:
badges.append(BADGE_ICONS[orb_tier])
default_avatar = get_default_avatar(
target_user.id, user_data.get("discriminator", "0")
)
avatar_url = target_user.avatar.url if target_user.avatar else default_avatar
banner_url = None
banner_file = None
original_banner_link = None
if banner_hash:
is_animated = banner_hash.startswith("a_")
ext = "gif" if is_animated else "png"
original_banner_url = f"https://cdn.discordapp.com/banners/{target_user.id}/{banner_hash}.{ext}?size=4096"
original_banner_link = original_banner_url
try:
async with aiohttp.ClientSession() as session:
async with session.get(original_banner_url) as resp:
if resp.status == 200:
banner_data = await resp.read()
img = Image.open(BytesIO(banner_data))
if img.width < 1100:
target_width = 1100
target_height = 440
img_aspect = img.width / img.height
target_aspect = target_width / target_height
if img_aspect > target_aspect:
scale_height = target_height
scale_width = int(scale_height * img_aspect)
else:
scale_width = target_width
scale_height = int(scale_width / img_aspect)
left = (scale_width - target_width) // 2
top = (scale_height - target_height) // 2
right = left + target_width
bottom = top + target_height
if is_animated:
frames = []
durations = []
try:
while True:
frame = img.copy().convert("RGBA")
frame = frame.resize(
(scale_width, scale_height),
Image.Resampling.LANCZOS,
)
frame = frame.crop(
(left, top, right, bottom)
)
frames.append(frame)
durations.append(
img.info.get("duration", 100)
)
img.seek(img.tell() + 1)
except EOFError:
pass
output = BytesIO()
frames[0].save(
output,
format="GIF",
save_all=True,
append_images=frames[1:],
duration=durations,
loop=0,
optimize=False,
)
output.seek(0)
banner_file = discord.File(
output, filename="banner.gif"
)
banner_url = "attachment://banner.gif"
else:
img = img.resize(
(scale_width, scale_height),
Image.Resampling.LANCZOS,
)
img = img.crop((left, top, right, bottom))
output = BytesIO()
img.save(output, format="PNG")
output.seek(0)
banner_file = discord.File(
output, filename="banner.png"
)
banner_url = "attachment://banner.png"
else:
banner_url = original_banner_url
except:
banner_url = original_banner_url
images = [f"[Avatar]({avatar_url})"]
if banner_url:
images.append(f"[Banner]({original_banner_link})")
if avatar_decoration:
await get_published_listing(bot, avatar_decoration["sku_id"])
decoration_url = f"https://cdn.discordapp.com/avatar-decoration-presets/{avatar_decoration['asset']}.png?size=4096&passthrough=true"
images.append(f"[Avatar Deco]({decoration_url})")
collectibles = user_data.get("collectibles")
if collectibles and collectibles.get("nameplate"):
nameplate = collectibles["nameplate"]
nameplate_asset = nameplate["asset"]
images.append(
f"[Nameplate](https://cdn.discordapp.com/assets/collectibles/{nameplate_asset}static.png)"
)
mutual_guilds = [g for g in bot.guilds if g.get_member(target_user.id)]
display_name = user_data.get("global_name") or user_data.get("username")
desc_lines = [
f"# {member.nick if member and member.nick else display_name}",
f"{format_username(target_user).replace('@', '')} • <@{target_user.id}>",
]
subline = ""
if badges:
subline += "".join(badges)
activity_lines = []
if member and member.activities:
for activity in member.activities:
if activity.type == discord.ActivityType.custom:
activity_lines.append(ACTIVITY_TYPE_NAMES[4])
elif activity.type in [
discord.ActivityType.playing,
discord.ActivityType.listening,
discord.ActivityType.watching,
]:
name = activity.name
activity_lines.append(
f"{ACTIVITY_TYPE_ICONS.get(activity.type.value, '')} {ACTIVITY_TYPE_NAMES[activity.type.value]} **{name}**".strip()
)
if subline:
desc_lines.append(subline)
if mutual_guilds:
desc_lines.append(
f"-# {len(mutual_guilds)} Bot Mutual Server{'s' if len(mutual_guilds) > 1 else ''}"
)
else:
desc_lines.append("")
is_system = user_data.get("system") or user_data.get("discriminator") == "0000"
if bot_deleted and not is_system:
desc_lines.append(
"*This bot's application has been deleted*\n-# (or app ID and user ID desync)"
)
if is_system:
desc_lines.append("**System account**")
desc_lines.append("")
if activity_lines:
desc_lines.extend(activity_lines)
embed = discord.Embed(color=0x7289DA, description="\n".join(desc_lines))
primary_guild = user_data.get("primary_guild")
if primary_guild and primary_guild.get("identity_guild_id"):
clan_badge_url = f"https://cdn.discordapp.com/clan-badges/{primary_guild['identity_guild_id']}/{primary_guild['badge']}.png?size=4096"
embed.set_author(name=primary_guild.get("tag", ""), icon_url=clan_badge_url)
else:
embed.set_author(
name="User Information",
icon_url="https://yes.nighty.works/raw/gSxqzV.png",
)
if member and member.nick and member.nick != display_name:
embed.title = display_name
embed.set_thumbnail(url=avatar_url)
if banner_url:
embed.set_image(url=banner_url)
created_timestamp = int(snowflake_to_timestamp(target_user.id))
created_date = f"<t:{created_timestamp}:F>"
embed.add_field(name="Created Date", value=created_date, inline=False)
if member and hasattr(member, "joined_at") and member.joined_at:
joined_timestamp = int(member.joined_at.timestamp())
join_date = f"<t:{joined_timestamp}:F>"
embed.add_field(name="Join Date", value=join_date, inline=False)
is_bot = user_data.get("bot", False)
embed.add_field(name="Is Bot", value="True" if is_bot else "False", inline=True)
if member and member.roles[1:]:
roles = sorted(member.roles[1:], key=lambda r: r.position, reverse=True)
role_mentions = [f"<@&{r.id}>" for r in roles]
role_list = " ".join(role_mentions)
if len(role_list) > 1024:
truncated_roles = []
current_length = 0
for mention in role_mentions:
if current_length + len(mention) + 1 > 1020:
break
truncated_roles.append(mention)
current_length += len(mention) + 1
role_list = " ".join(truncated_roles) + " ..."
embed.add_field(
name=f"Roles ({len(member.roles) - 1})", value=role_list, inline=False
)
if images:
embed.add_field(name="\u200b", value="\u3000".join(images), inline=False)
embed.set_footer(text=f"ID: {target_user.id}")
if banner_file:
await context.send(embed=embed, file=banner_file)
else:
await context.send(embed=embed)
return userinfo

261
cogs/help.py Normal file
View File

@@ -0,0 +1,261 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Help(commands.Cog, name="help"):
def __init__(self, bot) -> None:
self.bot = bot
async def category_autocomplete(
self,
interaction: discord.Interaction,
current: str,
) -> list[app_commands.Choice[str]]:
categories = [
"general",
"fun",
"moderation",
"owner",
"sidestore",
"idevice",
"melonx",
"livecontainer",
"media",
"miscellaneous",
"utilities",
"events",
]
suggestions = []
for category in categories:
if current.lower() in category.lower():
suggestions.append(
app_commands.Choice(
name=f"{category.capitalize()} Commands", value=category
)
)
return suggestions[:25]
@commands.hybrid_command(
name="help", description="List all commands the bot has loaded."
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(category="Choose a specific category to view its commands")
@app_commands.autocomplete(category=category_autocomplete)
async def help(self, context: Context, category: str = None) -> None:
category_mapping = {
"general": "general",
"fun": "fun",
"idevice": "idevice",
"melonx": "melonx",
"livecontainer": "livecontainer",
"media": "media",
"misc": "miscellaneous",
"miscellaneous": "miscellaneous",
"moderation": "moderation",
"sidestore": "sidestore",
"utils": "utilities",
"utilities": "utilities",
"events": "events",
"sync": "owner",
"logs": "owner",
"invite": "owner",
"load": "owner",
"unload": "owner",
"reload": "owner",
"shutdown": "owner",
"say": "owner",
}
category_descriptions = {
"general": "General commands",
"fun": "Fun commands",
"moderation": "Administration commands",
"owner": "Owner commands",
"sidestore": "SideStore troubleshooting commands",
"idevice": "idevice troubleshooting commands",
"melonx": "MeloNX troubleshooting commands",
"livecontainer": "LiveContainer troubleshooting commands",
"media": "Media commands",
"utilities": "Utility commands",
"miscellaneous": "Miscellaneous commands",
"events": "Events commands",
}
if category is None:
embed = discord.Embed(title="Help", color=0x7289DA)
embed.set_author(
name="Help", icon_url="https://yes.nighty.works/raw/T9mnBO.png"
)
standalone_commands = []
botinfo_cmd = self.bot.tree.get_command("botinfo")
if botinfo_cmd:
standalone_commands.append(
"**/botinfo** » Get information about this bot"
)
if standalone_commands:
embed.add_field(
name="", value="".join(standalone_commands) + "\n", inline=False
)
available_categories = set()
for cog_name in self.bot.cogs:
mapped_category = category_mapping.get(cog_name.lower())
if mapped_category:
available_categories.add(mapped_category)
category_list = []
for cat in sorted(available_categories):
description = category_descriptions.get(
cat, f"{cat.capitalize()} commands"
)
category_list.append(f"**/help {cat}** » {description}")
if category_list:
embed.add_field(name="", value="\n".join(category_list), inline=False)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
try:
await context.send(embed=embed)
except (discord.Forbidden, discord.HTTPException):
try:
await context.author.send(embed=embed)
except discord.Forbidden:
pass # User has DMs disabled
return
category = category.lower()
if category not in category_descriptions:
embed = discord.Embed(
title="Error",
description=f"Category '{category}' not found. Use `/help` to see available categories.",
color=0x7289DA,
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
try:
await context.send(embed=embed)
except (discord.Forbidden, discord.HTTPException):
try:
await context.author.send(embed=embed)
except discord.Forbidden:
pass
return
commands_in_category = []
seen_names = set()
for cog_name in self.bot.cogs:
if category_mapping.get(cog_name.lower()) == category:
cog = self.bot.get_cog(cog_name)
if cog:
commands_list = cog.get_commands()
for command in commands_list:
has_prefix_subcommands = (
hasattr(command, "commands")
and len(getattr(command, "commands", [])) > 0
)
if has_prefix_subcommands:
continue
name = command.name
if name in seen_names:
continue
description = (
command.description.partition("\n")[0]
if command.description
else "No description available"
)
commands_in_category.append((name, description))
seen_names.add(name)
for app_command in self.bot.tree.get_commands():
bound_cog = getattr(app_command, "binding", None)
if bound_cog is None:
continue
bound_cog_name = getattr(bound_cog, "qualified_name", "").lower()
if category_mapping.get(bound_cog_name) != category:
continue
has_subcommands = (
hasattr(app_command, "commands")
and len(getattr(app_command, "commands", [])) > 0
)
if has_subcommands and category not in ["owner"]:
for subcommand in app_command.commands:
if subcommand.name in seen_names:
continue
sub_desc = (
subcommand.description.partition("\n")[0]
if getattr(subcommand, "description", None)
else "No description available"
)
commands_in_category.append(
(f"{app_command.name} {subcommand.name}", sub_desc)
)
seen_names.add(f"{app_command.name} {subcommand.name}")
else:
if app_command.name in seen_names:
continue
description = (
app_command.description.partition("\n")[0]
if getattr(app_command, "description", None)
else "No description available"
)
commands_in_category.append((app_command.name, description))
seen_names.add(app_command.name)
if not commands_in_category:
embed = discord.Embed(
title="Error",
description=f"No commands found in category '{category}'.",
color=0x7289DA,
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
try:
await context.send(embed=embed)
except (discord.Forbidden, discord.HTTPException):
try:
await context.author.send(embed=embed)
except discord.Forbidden:
pass
return
embed = discord.Embed(title=f"/help » {category.lower()}", color=0x7289DA)
embed.set_author(
name="Help", icon_url="https://yes.nighty.works/raw/T9mnBO.png"
)
data = []
for command_name, description in sorted(commands_in_category):
data.append(f"**/{command_name}** » {description}")
help_text = "\n".join(data)
embed.add_field(name="", value=help_text, inline=False)
if context.interaction:
await context.interaction.response.send_message(embed=embed, ephemeral=True)
else:
try:
await context.send(embed=embed)
except (discord.Forbidden, discord.HTTPException):
try:
await context.author.send(embed=embed)
except discord.Forbidden:
pass
async def setup(bot) -> None:
await bot.add_cog(Help(bot))

117
cogs/idevice/__init__.py Normal file
View File

@@ -0,0 +1,117 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .idevice import ideviceView
from .error_codes import errorcodes_command
from .developermode import developermode_command
from .noapps import noapps_command
from .mountddi import mountddi_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Idevice(commands.GroupCog, name="idevice"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="idevice", invoke_without_command=True)
async def idevice_group(self, context: Context):
embed = discord.Embed(
title="idevice Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0xFA8C4A,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
view = ideviceView(self.bot)
await context.send(embed=embed, view=view)
@idevice_group.command(name="errorcodes")
async def idevice_group_errorcodes(
self, context: Context, *, error_code: str = None
):
await self._invoke_hybrid(context, "errorcodes", error_code=error_code)
@idevice_group.command(name="developermode")
async def idevice_group_developermode(self, context: Context):
await self._invoke_hybrid(context, "developermode")
@idevice_group.command(name="noapps")
async def idevice_group_noapps(self, context: Context):
await self._invoke_hybrid(context, "noapps")
@idevice_group.command(name="mountddi")
async def idevice_group_mountddi(self, context: Context):
await self._invoke_hybrid(context, "mountddi")
async def _invoke_hybrid(self, context: Context, name: str, **kwargs):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command, **kwargs)
else:
await context.send(f"Unknown idevice command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@app_commands.command(name="help", description="idevice troubleshooting help")
async def help(self, interaction: discord.Interaction):
embed = discord.Embed(
title="idevice Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0xFA8C4A,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
view = ideviceView(self.bot)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="errorcodes", description="Look up error codes and their meanings."
)
async def errorcodes(self, context, *, error_code: str = None):
return await errorcodes_command()(self, context, name=error_code)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="developermode", description="How to turn on developer mode"
)
async def developermode(self, context):
return await developermode_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="noapps",
description="Help when apps aren't showing in installed apps view",
)
async def noapps(self, context):
return await noapps_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="mountddi", description="How to manually mount DDI")
async def mountddi(self, context):
return await mountddi_command()(self, context)
async def setup(bot) -> None:
cog = Idevice(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'idevice.help'")
bot.logger.info("Loaded extension 'idevice.errorcodes'")
bot.logger.info("Loaded extension 'idevice.developermode'")
bot.logger.info("Loaded extension 'idevice.noapps'")
bot.logger.info("Loaded extension 'idevice.mountddi'")

View File

@@ -0,0 +1,42 @@
import discord
from discord.ext import commands
def developermode_command():
@commands.hybrid_command(
name="developermode", description="How to turn on developer mode"
)
async def developermode(self, context):
embed = discord.Embed(
color=0xFA8C4A,
description=(
"# How to Enable Developer Mode\n\n---\n\n"
+ '1. Open the "Settings" app\n'
+ '2. Navigate to "Privacy & Security"\n'
+ '3. Scroll all the way down to find "Developer Mode"\n\n'
+ "If you don't see the Developer Mode option, you need to install a developer app first.\n\n"
+ "You can use [SideStore](https://sidestore.io/) for this purpose - follow their installation guide to get started."
),
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
embed.set_footer(text="Last Edited by neoarz")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/idevice/developermode.py",
emoji="<:githubicon:1417717356846776340>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return developermode

View File

@@ -0,0 +1,84 @@
import json
import os
import discord
from discord import app_commands
from discord.ext import commands
def errorcodes_command():
@commands.hybrid_command(
name="errorcodes", description="Look up an idevice error code by name or number"
)
@app_commands.describe(name="Start typing to search all error names and codes")
async def errorcodes(self, context, name: str | None = None):
if name is None:
if context.interaction:
await context.interaction.response.send_message(
"Please provide an error code.", ephemeral=True
)
else:
await context.send("Please provide an error code.")
return
def load_errors():
json_path = os.path.join(os.path.dirname(__file__), "files/errorcodes.json")
try:
with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
self.bot.logger.error(f"Error codes JSON file not found: {json_path}")
return []
except json.JSONDecodeError as e:
self.bot.logger.error(f"Error parsing error codes JSON: {e}")
return []
errors = load_errors()
key_to_data = {
error["name"]: (error["description"], error["code"]) for error in errors
}
code_to_key = {error["code"]: error["name"] for error in errors}
key = name
if key not in key_to_data:
try:
num = int(name)
key = code_to_key.get(num)
if key is None and num > 0:
key = code_to_key.get(-num)
except ValueError:
key = None
if key is None or key not in key_to_data:
if context.interaction:
await context.interaction.response.send_message(
"Error not found.", ephemeral=True
)
else:
await context.send("Error not found.")
return
title, code = key_to_data[key]
embed = discord.Embed(
description=f"## Error Code: {code}\n\n**Name**: `{key}`\n**Description**: {title}",
color=0xFA8C4A,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/idevice/error_codes.py",
emoji="<:githubicon:1417717356846776340>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return errorcodes

View File

@@ -0,0 +1,337 @@
[
{
"name": "socket",
"description": "device socket io failed",
"code": -1
},
{
"name": "pem_parse_failed",
"description": "PEM parse failed",
"code": -2
},
{
"name": "rustls",
"description": "TLS error",
"code": -3
},
{
"name": "tls_builder_failed",
"description": "TLS verifiction build failed",
"code": -4
},
{
"name": "plist",
"description": "io on plist",
"code": -5
},
{
"name": "utf8",
"description": "can't convert bytes to utf8",
"code": -6
},
{
"name": "unexpected_response",
"description": "unexpected response from device",
"code": -7
},
{
"name": "get_prohibited",
"description": "this request was prohibited",
"code": -8
},
{
"name": "session_inactive",
"description": "no SSL session is active",
"code": -9
},
{
"name": "invalid_host_id",
"description": "device does not have pairing file",
"code": -10
},
{
"name": "no_established_connection",
"description": "no established connection",
"code": -11
},
{
"name": "heartbeat_sleepy_time",
"description": "device went to sleep",
"code": -12
},
{
"name": "heartbeat_timeout",
"description": "heartbeat timeout",
"code": -13
},
{
"name": "not_found",
"description": "not found",
"code": -14
},
{
"name": "service_not_found",
"description": "service not found",
"code": -15
},
{
"name": "cdtunnel_packet_too_short",
"description": "CDTunnel packet too short",
"code": -16
},
{
"name": "cdtunnel_packet_invalid_magic",
"description": "CDTunnel packet invalid magic",
"code": -17
},
{
"name": "packet_size_mismatch",
"description": "Proclaimed packet size does not match actual size",
"code": -18
},
{
"name": "json",
"description": "JSON serialization failed",
"code": -19
},
{
"name": "device_not_found",
"description": "device not found",
"code": -20
},
{
"name": "device_locked",
"description": "device lockded",
"code": -21
},
{
"name": "usb_connection_refused",
"description": "device refused connection",
"code": -22
},
{
"name": "usb_bad_command",
"description": "bad command",
"code": -23
},
{
"name": "usb_bad_device",
"description": "bad device",
"code": -24
},
{
"name": "usb_bad_version",
"description": "usb bad version",
"code": -25
},
{
"name": "bad_build_manifest",
"description": "bad build manifest",
"code": -26
},
{
"name": "image_not_mounted",
"description": "image not mounted",
"code": -27
},
{
"name": "pairing_dialog_response_pending",
"description": "pairing trust dialog pending",
"code": -28
},
{
"name": "user_denied_pairing",
"description": "user denied pairing trust",
"code": -29
},
{
"name": "password_protected",
"description": "device is locked",
"code": -30
},
{
"name": "misagent_failure",
"description": "misagent operation failed",
"code": -31
},
{
"name": "installation_proxy_operation_failed",
"description": "installation proxy operation failed",
"code": -32
},
{
"name": "afc",
"description": "afc error",
"code": -33
},
{
"name": "unknown_afc_opcode",
"description": "unknown afc opcode",
"code": -34
},
{
"name": "invalid_afc_magic",
"description": "invalid afc magic",
"code": -35
},
{
"name": "afc_missing_attribute",
"description": "missing file attribute",
"code": -36
},
{
"name": "crash_report_mover_bad_response",
"description": "crash report mover sent the wrong response",
"code": -37
},
{
"name": "reqwest",
"description": "http reqwest error",
"code": -38
},
{
"name": "internal_error",
"description": "internal error",
"code": -39
},
{
"name": "unknown_frame",
"description": "unknown http frame type",
"code": -40
},
{
"name": "unknown_http_setting",
"description": "unknown http setting type",
"code": -41
},
{
"name": "uninitialized_stream_id",
"description": "Unintialized stream ID",
"code": -42
},
{
"name": "unknown_xpc_type",
"description": "unknown XPC type",
"code": -43
},
{
"name": "malformed_xpc",
"description": "malformed XPC message",
"code": -44
},
{
"name": "invalid_xpc_magic",
"description": "invalid XPC magic",
"code": -45
},
{
"name": "unexpected_xpc_version",
"description": "unexpected XPC version",
"code": -46
},
{
"name": "invalid_c_string",
"description": "invalid C string",
"code": -47
},
{
"name": "http_stream_reset",
"description": "stream reset",
"code": -48
},
{
"name": "http_go_away",
"description": "go away packet received",
"code": -49
},
{
"name": "ns_keyed_archive_error",
"description": "NSKeyedArchive error",
"code": -50
},
{
"name": "unknown_aux_value_type",
"description": "Unknown aux value type",
"code": -51
},
{
"name": "unknown_channel",
"description": "unknown channel",
"code": -52
},
{
"name": "addr_parse_error",
"description": "cannot parse string as IpAddr",
"code": -53
},
{
"name": "disable_memory_limit_failed",
"description": "disable memory limit failed",
"code": -54
},
{
"name": "not_enough_bytes",
"description": "not enough bytes",
"code": -55
},
{
"name": "utf8_error",
"description": "failed to parse bytes as valid utf8",
"code": -56
},
{
"name": "invalid_argument",
"description": "invalid argument passed",
"code": -57
},
{
"name": "unknown_error_type",
"description": "unknown error returned from device",
"code": -59
},
{
"name": "ffi_invalid_arg",
"description": "invalid arguments were passed",
"code": -60
},
{
"name": "ffi_invalid_string",
"description": "invalid string was passed",
"code": -61
},
{
"name": "ffi_buffer_too_small",
"description": "buffer passed is too small",
"code": -62
},
{
"name": "unsupported_watch_key",
"description": "unsupported watch key",
"code": -63
},
{
"name": "malformed_command",
"description": "malformed command",
"code": -64
},
{
"name": "integer_overflow",
"description": "integer overflow",
"code": -65
},
{
"name": "canceled_by_user",
"description": "canceled by user",
"code": -66
},
{
"name": "malformed_package_archive",
"description": "malformed package archive",
"code": -67
},
{
"name": "developer_mode_not_enabled",
"description": "Developer mode is not enabled",
"code": -68
}
]

View File

@@ -1,3 +1,351 @@
async def setup(bot) -> None:
pass
import discord
from discord.ext import commands
import json
import os
import math
def load_error_codes():
try:
json_path = os.path.join(os.path.dirname(__file__), "files/errorcodes.json")
with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
except json.JSONDecodeError:
return []
class ErrorCodesBrowserView(discord.ui.View):
def __init__(self, items_per_page=9):
super().__init__(timeout=300)
self.error_codes = load_error_codes()
self.items_per_page = items_per_page
self.current_page = 0
self.max_pages = (
math.ceil(len(self.error_codes) / items_per_page) if self.error_codes else 1
)
self.update_buttons()
def update_buttons(self):
self.clear_items()
first_button = discord.ui.Button(
emoji="<:leftmax:1420240325770870905>",
style=discord.ButtonStyle.primary,
disabled=(self.current_page == 0),
)
first_button.callback = self.first_page
self.add_item(first_button)
prev_button = discord.ui.Button(
emoji="<:left:1420240344926126090>",
style=discord.ButtonStyle.primary,
disabled=(self.current_page == 0),
)
prev_button.callback = self.prev_page
self.add_item(prev_button)
stop_button = discord.ui.Button(
emoji="<:middle:1420240356087173160>", style=discord.ButtonStyle.secondary
)
stop_button.callback = self.stop_interaction
self.add_item(stop_button)
next_button = discord.ui.Button(
emoji="<:right:1420240334100627456>",
style=discord.ButtonStyle.primary,
disabled=(self.current_page >= self.max_pages - 1),
)
next_button.callback = self.next_page
self.add_item(next_button)
last_button = discord.ui.Button(
emoji="<:rightmax:1420240368846372886>",
style=discord.ButtonStyle.primary,
disabled=(self.current_page >= self.max_pages - 1),
)
last_button.callback = self.last_page
self.add_item(last_button)
def create_embed(self):
if not self.error_codes:
embed = discord.Embed(
title="Error Codes", description="No error codes found.", color=0xFA8C4A
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
embed.set_footer(text="Page 0 of 0")
return embed
embed = discord.Embed(title="Error Codes", color=0xFA8C4A)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
start_idx = self.current_page * self.items_per_page
end_idx = min(start_idx + self.items_per_page, len(self.error_codes))
current_codes = self.error_codes[start_idx:end_idx]
for i, error in enumerate(current_codes):
field_value = f"**Code:** `{error.get('code', 'N/A')}`\n**Name:** `{error.get('name', 'Unknown')}`\n**Description:** {error.get('description', 'No description')}"
embed.add_field(name="\u200b", value=field_value, inline=True)
while len(current_codes) % 3 != 0:
embed.add_field(name="\u200b", value="\u200b", inline=True)
embed.set_footer(text=f"Page {self.current_page + 1} of {self.max_pages}")
return embed
async def first_page(self, interaction: discord.Interaction):
self.current_page = 0
self.update_buttons()
embed = self.create_embed()
await interaction.response.edit_message(embed=embed, view=self)
async def prev_page(self, interaction: discord.Interaction):
if self.current_page > 0:
self.current_page -= 1
self.update_buttons()
embed = self.create_embed()
await interaction.response.edit_message(embed=embed, view=self)
async def stop_interaction(self, interaction: discord.Interaction):
self.clear_items()
embed = self.create_embed()
embed.set_footer(text="Interaction stopped")
await interaction.response.edit_message(embed=embed, view=self)
self.stop()
async def next_page(self, interaction: discord.Interaction):
if self.current_page < self.max_pages - 1:
self.current_page += 1
self.update_buttons()
embed = self.create_embed()
await interaction.response.edit_message(embed=embed, view=self)
async def last_page(self, interaction: discord.Interaction):
self.current_page = self.max_pages - 1
self.update_buttons()
embed = self.create_embed()
await interaction.response.edit_message(embed=embed, view=self)
class ideviceSelect(discord.ui.Select):
def __init__(self, bot):
self.bot = bot
options = [
discord.SelectOption(
label="No Apps",
value="noapps",
description="Help when apps aren't showing in installed apps view",
),
discord.SelectOption(
label="Error Codes",
value="errorcodes",
description="Browse idevice error codes",
),
discord.SelectOption(
label="Developer Mode",
value="developermode",
description="How to turn on developer mode",
),
discord.SelectOption(
label="Mount DDI",
value="mountddi",
description="How to manually mount DDI",
),
]
super().__init__(placeholder="Choose an idevice command...", options=options)
async def callback(self, interaction: discord.Interaction):
command_name = self.values[0]
if command_name == "errorcodes":
try:
view = ErrorCodesBrowserView()
embed = view.create_embed()
success_embed = discord.Embed(
title="Command Executed",
description="Successfully executed `/errorcodes`. Please run /[errorcode_number] to get more information about an error code, and send it in chat",
color=0x00FF00,
)
success_embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
await interaction.response.edit_message(embed=success_embed, view=None)
await interaction.followup.send(embed=embed, view=view, ephemeral=True)
except discord.Forbidden:
guild_info = (
f"server {interaction.guild.name} (ID: {interaction.guild.id})"
if interaction.guild
else "DM or private channel"
)
self.bot.logger.warning(
f"Bot missing permissions in {guild_info} - cannot execute errorcodes command"
)
if interaction.guild is None:
embed = discord.Embed(
title="Error",
description="This command cannot be executed in DMs.",
color=0xFF0000,
)
else:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xFF0000,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
await interaction.response.edit_message(embed=embed, view=None)
except Exception as e:
self.bot.logger.error(f"Error executing errorcodes command: {e}")
embed = discord.Embed(
title="Error",
description="An error occurred while executing the command.",
color=0xFF0000,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
await interaction.response.edit_message(embed=embed, view=None)
return
command = self.bot.get_command(command_name)
if command:
try:
ctx = await self.bot.get_context(interaction.message)
if ctx:
await ctx.invoke(command)
embed = discord.Embed(
title="Command Executed",
description=f"Successfully executed `/{command_name}`",
color=0x00FF00,
)
embed.set_author(
name="idevice",
icon_url="https://yes.nighty.works/raw/snLMuO.png",
)
await interaction.response.edit_message(embed=embed, view=None)
except discord.Forbidden:
guild_info = (
f"server {interaction.guild.name} (ID: {interaction.guild.id})"
if interaction.guild
else "DM or private channel"
)
self.bot.logger.warning(
f"Bot missing permissions in {guild_info} - cannot execute {command_name} command"
)
if interaction.guild is None:
embed = discord.Embed(
title="Error",
description="This command cannot be executed in DMs.",
color=0xFF0000,
)
else:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xFF0000,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
await interaction.response.edit_message(embed=embed, view=None)
except Exception as e:
self.bot.logger.error(f"Error executing {command_name} command: {e}")
embed = discord.Embed(
title="Error",
description="An error occurred while executing the command.",
color=0xFF0000,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
await interaction.response.edit_message(embed=embed, view=None)
else:
embed = discord.Embed(
title="Error", description="Command not found!", color=0xFF0000
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
await interaction.response.edit_message(embed=embed, view=None)
class ideviceView(discord.ui.View):
def __init__(self, bot):
super().__init__()
self.add_item(ideviceSelect(bot))
def idevice_command():
@commands.hybrid_command(
name="idevice", description="idevice troubleshooting and help"
)
async def idevice(self, context):
if isinstance(context.channel, discord.DMChannel):
embed = discord.Embed(
title="Error",
description="This command can only be used in servers.",
color=0xE02B2B,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
await context.send(embed=embed, ephemeral=True)
return
if isinstance(context.channel, discord.PartialMessageable):
embed = discord.Embed(
title="Error",
description="The bot needs send messages permissions in this channel.",
color=0xE02B2B,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
await context.send(embed=embed, ephemeral=True)
return
embed = discord.Embed(
title="idevice Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0xFA8C4A,
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
view = ideviceView(self.bot)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, view=view, ephemeral=True
)
else:
await context.send(embed=embed, view=view)
return idevice

97
cogs/idevice/mountddi.py Normal file
View File

@@ -0,0 +1,97 @@
import discord
from discord.ext import commands
import os
import aiohttp
import shutil
import tempfile
def mountddi_command():
@commands.hybrid_command(name="mountddi", description="How to manually mount DDI")
async def mountddi(self, context):
await context.defer()
embed = discord.Embed(
color=0xFA8C4A,
description=(
"# How to Mount your DDI (Developer Disk Image):\n\n---\n\n"
"1. Ensure you are connected to StikDebug's VPN and Wi-Fi.*\n"
"2. Force close StikDebug from the app switcher, then repon it.\n"
"## This should resolve your error! Remember, this must be done every time you restart your device.\n"
"If it doesn't work after a couple tries or you live in a country where github.com is blocked, try the steps below to manually mount the DDI:*\n"
"1. **Download the DDI.zip file attached above:**\n"
" - Save it to your device and extract the contents\n\n"
"2. **Replace the DDI folder in StikDebug:**\n"
" - Navigate to the StikDebug default directory on your iPhone/iPad\n"
" - Delete the existing DDI folder completely\n"
" - Replace it with the DDI folder from uncompressing the downloaded zip\n"
" - Make sure it's in StikDebug's default directory\n\n"
"3. **Restart and retry:**\n"
" - Completely restart StikDebug\n"
" - If you still get the same error, ask the idevice server for more help\n\n"
),
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/idevice/mountddi.py",
emoji="<:githubicon:1417717356846776340>",
)
)
temp_dir = tempfile.mkdtemp()
try:
ddi_dir = os.path.join(temp_dir, "DDI")
os.makedirs(ddi_dir)
base_url = "https://raw.githubusercontent.com/doronz88/DeveloperDiskImage/main/PersonalizedImages/Xcode_iOS_DDI_Personalized"
files = ["BuildManifest.plist", "Image.dmg", "Image.dmg.trustcache"]
async with aiohttp.ClientSession() as session:
for filename in files:
file_url = f"{base_url}/{filename}"
async with session.get(file_url) as response:
if response.status != 200:
await context.send(
f"Error: Failed to download {filename} (Status: {response.status})"
)
return
file_path = os.path.join(ddi_dir, filename)
with open(file_path, "wb") as f:
while True:
chunk = await response.content.read(8192)
if not chunk:
break
f.write(chunk)
zip_base_name = os.path.join(temp_dir, "DDI")
shutil.make_archive(zip_base_name, "zip", root_dir=temp_dir, base_dir="DDI")
zip_file_path = zip_base_name + ".zip"
if os.path.exists(zip_file_path):
await context.send(
embed=embed,
view=view,
file=discord.File(zip_file_path, filename="DDI.zip"),
)
else:
await context.send(
"Error: Failed to create zip file.", embed=embed, view=view
)
except Exception as e:
await context.send(f"An error occurred: {e}")
finally:
shutil.rmtree(temp_dir)
return mountddi

44
cogs/idevice/noapps.py Normal file
View File

@@ -0,0 +1,44 @@
import discord
from discord.ext import commands
def noapps_command():
@commands.hybrid_command(
name="noapps",
description="Help when apps aren't showing in installed apps view",
)
async def noapps(self, context):
embed = discord.Embed(
color=0xFA8C4A,
description=(
"# Apps Not Showing in Installed Apps View\n\n---\n\n"
+ "If apps aren't appearing in the StikDebug installed apps view, this is likely because they were signed with a distribution certificate instead of a development certificate.\n\n"
+ "Distribution certificates lack the `get-task-allow` entitlement needed for JIT.\n\n"
+ "To fix this issue:\n"
+ "- Use a development certificate when signing apps, or\n"
+ "- Try SideStore, the best free sideloading method available\n\n"
+ "More details can be found at [SideStore's official website](https://sidestore.io/)"
),
)
embed.set_author(
name="idevice", icon_url="https://yes.nighty.works/raw/snLMuO.png"
)
embed.set_footer(text="Last Edited by neoarz")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/idevice/noapps.py",
emoji="<:githubicon:1417717356846776340>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return noapps

View File

@@ -0,0 +1,93 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .livecontainer import LivecontainerView
from .jit26 import jit26_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Livecontainer(commands.GroupCog, name="livecontainer"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="livecontainer", invoke_without_command=True)
async def livecontainer_group(self, context: Context):
embed = discord.Embed(
title="LiveContainer Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x0169FF,
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
view = LivecontainerView(self.bot)
await context.send(embed=embed, view=view)
@livecontainer_group.command(name="help")
async def livecontainer_group_help(self, context: Context):
embed = discord.Embed(
title="LiveContainer Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x0169FF,
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
view = LivecontainerView(self.bot)
await context.send(embed=embed, view=view)
@livecontainer_group.command(name="26jit")
async def livecontainer_group_26jit(self, context: Context):
await self._invoke_hybrid(context, "26jit")
async def _invoke_hybrid(self, context: Context, name: str):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command)
else:
await context.send(f"Unknown LiveContainer command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@app_commands.command(name="help", description="LiveContainer troubleshooting help")
async def help(self, interaction: discord.Interaction):
embed = discord.Embed(
title="LiveContainer Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x0169FF,
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
view = LivecontainerView(self.bot)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="26jit", description="Walkthrough for iOS 26 JIT and sideloading"
)
async def jit26(self, context):
return await jit26_command()(self, context)
async def setup(bot) -> None:
cog = Livecontainer(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'livecontainer.help'")
bot.logger.info("Loaded extension 'livecontainer.26jit'")

View File

@@ -0,0 +1,38 @@
import discord
from discord.ext.commands import Context
def jit26_command():
async def command(self, context: Context):
embed = discord.Embed(
color=0x0169FF,
description=(
"# iOS 26 JIT & Sideloading Walkthrough\n\n---\n\n"
"Click the [button below](https://github.com/CelloSerenity/iOS-26-Sideloading-and-JIT-Complete-Walkthrough) to set up sideloading and enabling JIT for LiveContainer on iOS 26."
),
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
embed.set_footer(
icon_url="https://yes.nighty.works/raw/2PPWd3.webp",
text="Made By CelloSerenity",
)
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Get Started",
url="https://github.com/CelloSerenity/iOS-26-Sideloading-and-JIT-Complete-Walkthrough",
style=discord.ButtonStyle.primary,
emoji="<:githubicon:1417717356846776340>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return command

View File

@@ -0,0 +1,90 @@
import discord
class LivecontainerSelect(discord.ui.Select):
def __init__(self, bot):
self.bot = bot
options = [
discord.SelectOption(
label="iOS 26 JIT & Sideloading",
value="26jit",
description="Walkthrough for iOS 26 JIT and sideloading",
),
]
super().__init__(
placeholder="Choose a LiveContainer command...", options=options
)
async def callback(self, interaction: discord.Interaction):
command_name = self.values[0]
command = self.bot.get_command(command_name)
if command:
try:
ctx = await self.bot.get_context(interaction.message)
if ctx:
await ctx.invoke(command)
embed = discord.Embed(
title="Command Executed",
description=f"Successfully executed `/{command_name}`",
color=0x00FF00,
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
await interaction.response.edit_message(embed=embed, view=None)
except discord.Forbidden:
guild_info = (
f"server {interaction.guild.name} (ID: {interaction.guild.id})"
if interaction.guild
else "DM or private channel"
)
self.bot.logger.warning(
f"Bot missing permissions in {guild_info} - cannot execute {command_name} command"
)
if interaction.guild is None:
embed = discord.Embed(
title="Error",
description="This command cannot be executed in DMs.",
color=0xFF0000,
)
else:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xFF0000,
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
await interaction.response.edit_message(embed=embed, view=None)
except Exception as e:
self.bot.logger.error(f"Error executing {command_name} command: {e}")
embed = discord.Embed(
title="Error",
description="An error occurred while executing the command.",
color=0xFF0000,
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
await interaction.response.edit_message(embed=embed, view=None)
else:
embed = discord.Embed(
title="Error", description="Command not found!", color=0xFF0000
)
embed.set_author(
name="LiveContainer",
icon_url="https://raw.githubusercontent.com/LiveContainer/LiveContainer/main/screenshots/livecontainer_icon.png",
)
await interaction.response.edit_message(embed=embed, view=None)
class LivecontainerView(discord.ui.View):
def __init__(self, bot):
super().__init__()
self.add_item(LivecontainerSelect(bot))

143
cogs/media/__init__.py Normal file
View File

@@ -0,0 +1,143 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from typing import Optional
from .mcquote import mcquote_command
from .img2gif import img2gif_command
from .tweety import tweety_command
from .tts import tts_command
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Media(commands.GroupCog, name="media"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
"""Listen for bot mentions with 'tweety' command while replying to a message"""
if message.author.bot:
return
if (
self.bot.user in message.mentions
and message.reference
and message.reference.message_id
):
content = message.content.lower()
content_without_mention = (
content.replace(f"<@{self.bot.user.id}>", "")
.replace(f"<@!{self.bot.user.id}>", "")
.strip()
)
if content_without_mention.strip() == "tweety":
ctx = await self.bot.get_context(message)
await self.tweety(ctx)
@commands.group(name="media", invoke_without_command=True)
async def media_group(self, context: Context):
embed = discord.Embed(
title="Media Commands",
description="Use `.media <subcommand>` or `/media <subcommand>`.",
color=0x7289DA,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
embed.add_field(
name="Available",
value="mcquote, img2gif, tweety, tts",
inline=False,
)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str, *args, **kwargs):
if name == "mcquote":
await self.mcquote(context, text=kwargs.get("text", ""))
return
if name == "img2gif":
await self.img2gif(context, attachment=kwargs.get("attachment"))
return
if name == "tweety":
await self.tweety(context)
return
if name == "tts":
await self.tts(context, text=kwargs.get("text"))
return
await context.send(f"Unknown media command: {name}")
@media_group.command(name="mcquote")
async def media_group_mcquote(self, context: Context, *, text: str):
await self._invoke_hybrid(context, "mcquote", text=text)
@media_group.command(name="img2gif")
async def media_group_img2gif(
self, context: Context, attachment: Optional[discord.Attachment] = None
):
await self._invoke_hybrid(context, "img2gif", attachment=attachment)
@media_group.command(name="tweety")
async def media_group_tweety(self, context: Context):
await self._invoke_hybrid(context, "tweety")
@media_group.command(name="tts")
async def media_group_tts(self, context: Context, *, text: str = None):
await self._invoke_hybrid(context, "tts", text=text)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="mcquote",
description="Generate a custom Minecraft quote image.",
)
async def mcquote(self, context, *, text: str):
return await mcquote_command()(self, context, text=text)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="img2gif",
description="Convert an uploaded image to a GIF.",
)
async def img2gif(self, context, attachment: Optional[discord.Attachment] = None):
return await img2gif_command()(self, context, attachment=attachment)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="tweety",
description="Convert a replied message to a tweet image.",
)
async def tweety(self, context):
return await tweety_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="tts",
description="Convert text to speech using Google Text-to-Speech.",
)
async def tts(self, context, text: str = None):
return await tts_command()(context, text=text)
async def setup(bot) -> None:
cog = Media(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'media.mcquote'")
bot.logger.info("Loaded extension 'media.img2gif'")
bot.logger.info("Loaded extension 'media.tweety'")
bot.logger.info("Loaded extension 'media.tts'")

186
cogs/media/img2gif.py Normal file
View File

@@ -0,0 +1,186 @@
import os
import tempfile
import discord
from discord.ext import commands
from PIL import Image
import subprocess
import shutil
from typing import Optional
try:
import pillow_heif
pillow_heif.register_heif_opener()
except Exception:
pass
async def send_error_message(context, description: str):
embed = discord.Embed(
title="Error",
description=description,
color=0xE02B2B,
)
embed.set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp")
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
def img2gif_command():
@commands.hybrid_command(
name="img2gif",
description="Convert an uploaded image to a GIF.",
)
@commands.cooldown(1, 15, commands.BucketType.user)
async def img2gif(self, context, attachment: Optional[discord.Attachment] = None):
resolved_attachment = attachment
if resolved_attachment is None:
if (
context.message
and context.message.reference
and context.message.reference.resolved
):
ref_msg = context.message.reference.resolved
if isinstance(ref_msg, discord.Message) and ref_msg.attachments:
resolved_attachment = ref_msg.attachments[0]
if (
resolved_attachment is None
and context.message
and context.message.attachments
):
resolved_attachment = context.message.attachments[0]
if (
resolved_attachment is None
or not resolved_attachment.filename.lower().endswith(
(".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".heic", ".heif")
)
):
await send_error_message(
context,
"Provide or reply to an image (png/jpg/jpeg/webp/bmp/tiff/heic/heif).",
)
return
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.defer(ephemeral=False)
else:
processing_embed = discord.Embed(
title="Image to GIF (Processing)",
description="<a:mariospin:1423677027013103709> Converting image...",
color=0x7289DA,
)
processing_embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
processing_msg = await context.send(embed=processing_embed)
tmp_dir = tempfile.mkdtemp()
src_path = os.path.join(tmp_dir, resolved_attachment.filename)
out_path = os.path.join(
tmp_dir, os.path.splitext(resolved_attachment.filename)[0] + ".gif"
)
try:
await resolved_attachment.save(src_path)
src_for_pillow = src_path
try:
with Image.open(src_for_pillow) as img:
if img.mode not in ("RGB", "RGBA"):
img = img.convert("RGBA")
duration_ms = 100
loop = 0
img.save(
out_path,
format="GIF",
save_all=True,
optimize=True,
duration=duration_ms,
loop=loop,
)
except Exception:
if resolved_attachment.filename.lower().endswith(
(".heic", ".heif")
) and shutil.which("ffmpeg"):
png_path = os.path.join(
tmp_dir,
os.path.splitext(resolved_attachment.filename)[0] + ".png",
)
try:
subprocess.run(
["ffmpeg", "-y", "-i", src_path, png_path],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
with Image.open(png_path) as img:
if img.mode not in ("RGB", "RGBA"):
img = img.convert("RGBA")
duration_ms = 100
loop = 0
img.save(
out_path,
format="GIF",
save_all=True,
optimize=True,
duration=duration_ms,
loop=loop,
)
except Exception as conv_err:
raise conv_err
else:
raise
with open(out_path, "rb") as f:
file = discord.File(f, filename=os.path.basename(out_path))
if interaction is not None:
await interaction.followup.send(file=file)
else:
await processing_msg.delete()
await context.send(file=file)
except Exception as e:
if interaction is not None:
await interaction.followup.send(
embed=discord.Embed(
title="Error",
description=f"Failed to convert image: {e}",
color=0xE02B2B,
).set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
),
ephemeral=True,
)
else:
await processing_msg.delete()
await context.send(
embed=discord.Embed(
title="Error",
description=f"Failed to convert image: {e}",
color=0xE02B2B,
).set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
),
ephemeral=True,
)
finally:
try:
for f in os.listdir(tmp_dir):
try:
os.remove(os.path.join(tmp_dir, f))
except:
pass
os.rmdir(tmp_dir)
except:
pass
return img2gif

227
cogs/media/mcquote.py Normal file
View File

@@ -0,0 +1,227 @@
import os
import tempfile
import discord
from discord.ext import commands
import aiohttp
import random
def mcquote_command():
@commands.hybrid_command(
name="mcquote",
description="Generate a custom Minecraft quote image.",
)
@commands.cooldown(1, 10, commands.BucketType.user)
async def mcquote(self, context, *, text: str = None):
if isinstance(context.channel, discord.DMChannel):
embed = discord.Embed(
title="Error",
description="This command can only be used in servers.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if isinstance(context.channel, discord.PartialMessageable):
embed = discord.Embed(
title="Error",
description="The bot needs the `send messages` permission in this channel.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if not text:
embed = discord.Embed(
title="Error",
description="Please provide text for the Minecraft quote.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if len(text) > 25:
embed = discord.Embed(
title="Error",
description="Text must be 25 characters or less.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
# Check if bot has send messages permission before starting quote generation
try:
test_embed = discord.Embed(title="Testing permissions...", color=0x7289DA)
test_embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await context.channel.send(embed=test_embed, delete_after=0.1)
except discord.Forbidden:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
processing_embed = discord.Embed(
title="Minecraft Quote (Processing)",
description="<a:mariospin:1423677027013103709> Generating quote... This may take a moment.",
color=0x7289DA,
)
processing_embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(
embed=processing_embed, ephemeral=True
)
else:
await interaction.followup.send(embed=processing_embed, ephemeral=True)
else:
processing_msg = await context.send(embed=processing_embed)
quote_text = text.replace(" ", "+")
random_number = random.randint(1, 39)
mc_quote_url = f"https://skinmc.net/achievement/{random_number}/Achievement+Unlocked!/{quote_text}"
try:
async with aiohttp.ClientSession() as session:
async with session.get(mc_quote_url) as response:
response.raise_for_status()
content = await response.read()
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
temp_file.write(content)
temp_file_path = temp_file.name
embed = discord.Embed(
title="Minecraft Quote",
color=0x7289DA,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
embed.set_footer(
text=f"Requested by {context.author.name}",
icon_url=context.author.display_avatar.url,
)
with open(temp_file_path, "rb") as f:
file = discord.File(f, filename="mcquote.png")
interaction = getattr(context, "interaction", None)
if interaction is not None:
await context.channel.send(embed=embed)
await context.channel.send(file=file)
try:
await interaction.delete_original_response()
except:
pass
else:
await processing_msg.delete()
await context.channel.send(embed=embed)
await context.channel.send(file=file)
os.remove(temp_file_path)
except aiohttp.ClientError:
embed = discord.Embed(
title="Error",
description="Failed to generate Minecraft quote. Please try again later.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
try:
await interaction.delete_original_response()
except:
pass
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await processing_msg.delete()
await context.send(embed=embed, ephemeral=True)
except Exception as e:
embed = discord.Embed(
title="Error",
description=f"An unexpected error occurred: {str(e)}",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
try:
await interaction.delete_original_response()
except:
pass
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await processing_msg.delete()
await context.send(embed=embed, ephemeral=True)
return mcquote

255
cogs/media/tts.py Normal file
View File

@@ -0,0 +1,255 @@
import asyncio
import io
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands
from gtts import gTTS
DEFAULT_LANG = "en"
def tts_command():
async def send_embed(
context: commands.Context,
embed: discord.Embed,
*,
ephemeral: bool = False,
file: Optional[discord.File] = None,
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
if file:
await interaction.followup.send(
embed=embed, file=file, ephemeral=ephemeral
)
else:
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
if file:
await interaction.response.send_message(
embed=embed, file=file, ephemeral=ephemeral
)
else:
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
if file:
await context.send(embed=embed, file=file)
else:
await context.send(embed=embed)
async def generate_tts_audio(text: str) -> tuple[Optional[bytes], Optional[str]]:
try:
loop = asyncio.get_event_loop()
audio_bytes = await loop.run_in_executor(
None, lambda: _generate_tts_sync(text)
)
return audio_bytes, None
except Exception as e:
return None, str(e)
def _generate_tts_sync(text: str) -> bytes:
tts = gTTS(text=text, lang=DEFAULT_LANG, slow=False)
fp = io.BytesIO()
tts.write_to_fp(fp)
fp.seek(0)
return fp.read()
@commands.hybrid_command(
name="tts",
description="Convert text to speech using Google Text-to-Speech.",
)
@app_commands.describe(
text="The text to convert to speech",
)
async def tts(context: commands.Context, text: Optional[str] = None):
if isinstance(context.channel, discord.DMChannel):
embed = discord.Embed(
title="Error",
description="This command can only be used in servers.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if isinstance(context.channel, discord.PartialMessageable):
embed = discord.Embed(
title="Error",
description="The bot needs the `send messages` permission in this channel.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if not text or not text.strip():
if (
context.message
and context.message.reference
and context.message.reference.resolved
):
referenced = context.message.reference.resolved
if isinstance(referenced, discord.Message) and referenced.content:
text = referenced.content
if not text or not text.strip():
embed = discord.Embed(
title="Error",
description="Please provide text to convert or reply to a message containing text.",
color=0xE02B2B,
).set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await send_embed(context, embed, ephemeral=True)
return
text = text.strip()
if len(text) > 500:
embed = discord.Embed(
title="Error",
description="Text is too long. Please limit to 500 characters.",
color=0xE02B2B,
).set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await send_embed(context, embed, ephemeral=True)
return
# Check if bot has send messages permission before starting TTS generation
try:
test_embed = discord.Embed(title="Testing permissions...", color=0x7289DA)
test_embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await context.channel.send(embed=test_embed, delete_after=0.1)
except discord.Forbidden:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
processing_embed = discord.Embed(
title="TTS (Processing)",
description="<a:mariospin:1423677027013103709> Generating speech...",
color=0x7289DA,
).set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp")
interaction = getattr(context, "interaction", None)
processing_message = None
sent_initial_interaction_response = False
if interaction is not None:
if interaction.response.is_done():
processing_message = await interaction.followup.send(
embed=processing_embed, ephemeral=True
)
else:
await interaction.response.send_message(
embed=processing_embed, ephemeral=True
)
sent_initial_interaction_response = True
if not interaction.response.is_done():
await interaction.response.defer(ephemeral=False)
else:
processing_embed = discord.Embed(
title="TTS (Processing)",
description="<a:mariospin:1423677027013103709> Generating speech...",
color=0x7289DA,
).set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
processing_message = await context.send(embed=processing_embed)
audio_bytes, error = await generate_tts_audio(text)
if error or not audio_bytes:
embed = discord.Embed(
title="Error",
description=f"Failed to generate speech. {error or 'Unknown error.'}",
color=0xE02B2B,
).set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await send_embed(context, embed, ephemeral=True)
if interaction is not None and sent_initial_interaction_response:
try:
await interaction.delete_original_response()
except Exception:
pass
if processing_message:
try:
await processing_message.delete()
except Exception:
pass
return
audio_file = discord.File(
io.BytesIO(audio_bytes),
filename="audio.mp3",
)
embed = (
discord.Embed(
title="Text-to-Speech",
description=f"**Input:** {text}",
color=0x7289DA,
)
.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
.set_footer(
text=f"Requested by {context.author.display_name}",
icon_url=getattr(context.author.display_avatar, "url", None),
)
)
if interaction is not None:
await context.channel.send(embed=embed)
await context.channel.send(file=audio_file)
try:
await interaction.delete_original_response()
except:
pass
else:
await processing_message.delete()
await context.channel.send(embed=embed)
await context.channel.send(file=audio_file)
return tts

526
cogs/media/tweety.py Normal file
View File

@@ -0,0 +1,526 @@
# The API used in the tweety command is made by me and can be found here:
# https://github.com/neoarz/tweety-api
# I made this out of spite since i couldnt find any free APIs for this
# Its serverless and hosted on Vercel :)
import os
import tempfile
import discord
from discord.ext import commands
import aiohttp
from datetime import datetime
from typing import Optional
import pytz
import time
import asyncio
def break_long_words(text: str, max_word_length: int = 50) -> str:
words = text.split(" ")
result = []
for word in words:
if len(word) > max_word_length:
chunks = [
word[i : i + max_word_length]
for i in range(0, len(word), max_word_length)
]
result.append(" ".join(chunks))
else:
result.append(word)
return " ".join(result)
class TweetyHelpView(discord.ui.View):
"""This is the help view for the slash command cuz only the pinging and prefix versions can use this command"""
def __init__(self, user_id: int, bot):
super().__init__(timeout=180)
self.user_id = user_id
self.bot = bot
self.current_page = 0
self.pages = [
{
"title": "Tweety (Method 1)",
"description": "Use the prefix command `.media tweety` while replying to a message.",
"gif_url": "https://yes.nighty.works/raw/VrKX1L.gif",
"fields": [
{
"name": "How to use",
"value": "1. Reply to any message\n2. Type `.media tweety`\n3. Use the buttons to customize!",
"inline": False,
},
],
},
{
"title": "Tweety (Method 2)",
"description": f"Mention <@{bot.user.id}> with `tweety` while replying to a message.",
"gif_url": "https://yes.nighty.works/raw/9XEe9j.gif",
"fields": [
{
"name": "How to use",
"value": f"1. Reply to any message\n2. Type `<@{bot.user.id}> tweety`\n3. Use the buttons to customize!",
"inline": False,
},
],
},
]
self.update_buttons()
def create_embed(self):
page = self.pages[self.current_page]
embed = discord.Embed(
title=page["title"],
description=page["description"],
color=0x7289DA,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
for field in page["fields"]:
embed.add_field(
name=field["name"], value=field["value"], inline=field["inline"]
)
embed.set_image(url=page["gif_url"])
embed.set_footer(text=f"Page {self.current_page + 1}/{len(self.pages)}")
return embed
def update_buttons(self):
self.clear_items()
prev_button = discord.ui.Button(
label="Prev",
style=discord.ButtonStyle.secondary,
emoji=discord.PartialEmoji(name="left", id=1420240344926126090),
disabled=self.current_page == 0,
)
prev_button.callback = self.previous_page
self.add_item(prev_button)
next_button = discord.ui.Button(
label="Next",
style=discord.ButtonStyle.secondary,
emoji=discord.PartialEmoji(name="right", id=1420240334100627456),
disabled=self.current_page == len(self.pages) - 1,
)
next_button.callback = self.next_page
self.add_item(next_button)
async def previous_page(self, interaction: discord.Interaction):
if interaction.user.id != self.user_id:
await interaction.response.send_message(
"You can't control someone else's help menu!", ephemeral=True
)
return
if self.current_page > 0:
self.current_page -= 1
self.update_buttons()
await interaction.response.edit_message(
embed=self.create_embed(), view=self
)
else:
await interaction.response.defer()
async def next_page(self, interaction: discord.Interaction):
if interaction.user.id != self.user_id:
await interaction.response.send_message(
"You can't control someone else's help menu!", ephemeral=True
)
return
if self.current_page < len(self.pages) - 1:
self.current_page += 1
self.update_buttons()
await interaction.response.edit_message(
embed=self.create_embed(), view=self
)
else:
await interaction.response.defer()
async def on_timeout(self):
for item in self.children:
item.disabled = True
class TweetyView(discord.ui.View):
def __init__(
self,
author_id: int,
original_message,
tweet_data: dict,
api_url: str,
image_message: Optional[discord.Message] = None,
):
super().__init__(timeout=300)
self.author_id = author_id
self.original_message = original_message
self.tweet_data = tweet_data
self.api_url = api_url
self.is_dark = tweet_data.get("dark", False)
self.is_verified = tweet_data.get("verified", False)
self.image_message = image_message
self.last_regenerate_time = 0
self.click_count = 0
self.update_button_styles()
def update_button_styles(self):
self.clear_items()
dark_button = discord.ui.Button(
label="Dark Mode" if self.is_dark else "Light Mode",
style=discord.ButtonStyle.primary
if self.is_dark
else discord.ButtonStyle.secondary,
emoji=discord.PartialEmoji(name="darkmode", id=1425165393751965884),
custom_id="toggle_dark",
)
dark_button.callback = self.toggle_dark_callback
self.add_item(dark_button)
verified_button = discord.ui.Button(
label="Verified",
style=discord.ButtonStyle.primary
if self.is_verified
else discord.ButtonStyle.secondary,
emoji=discord.PartialEmoji(
name="TwitterVerifiedBadge", id=1425165432142172392
),
custom_id="toggle_verified",
)
verified_button.callback = self.toggle_verified_callback
self.add_item(verified_button)
async def regenerate_tweet(self, interaction: discord.Interaction):
"""Regenerate only the image message with current settings"""
if self.click_count >= 10:
embed = discord.Embed(
title="Error",
description="Stop spamming! You've reached the maximum number of changes for this tweet.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
await interaction.response.defer()
current_time = time.time()
time_since_last = current_time - self.last_regenerate_time
if time_since_last < 3 and self.last_regenerate_time > 0:
wait_time = 3 - time_since_last
await asyncio.sleep(wait_time)
self.click_count += 1
self.last_regenerate_time = time.time()
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.api_url}/api/render",
json=self.tweet_data,
headers={"Content-Type": "application/json"},
) as response:
if response.status != 200:
error_text = await response.text()
embed = discord.Embed(
title="Error",
description=f"API Error ({response.status}): {error_text}",
color=0xE02B2B,
)
embed.set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
)
await interaction.followup.send(embed=embed, ephemeral=True)
return
image_data = await response.read()
with tempfile.NamedTemporaryFile(
delete=False, suffix=".png"
) as temp_file:
temp_file.write(image_data)
temp_file_path = temp_file.name
with open(temp_file_path, "rb") as f:
author_name = self.original_message.author.name
filename = (
f"tweet_{author_name}_{int(datetime.now().timestamp())}.png"
)
file = discord.File(f, filename=filename)
self.update_button_styles()
if self.image_message is not None:
await self.image_message.edit(attachments=[file], view=self)
else:
await interaction.followup.send(file=file, view=self)
os.remove(temp_file_path)
except Exception:
embed = discord.Embed(
title="Error",
description="Error regenerating tweet image",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await interaction.followup.send(embed=embed, ephemeral=True)
async def _check_author(self, interaction: discord.Interaction) -> bool:
"""Check if user is authorized to modify the tweet"""
if interaction.user.id != self.author_id:
embed = discord.Embed(
title="Error",
description="You can't modify someone else's tweet!",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return False
return True
async def toggle_dark_callback(self, interaction: discord.Interaction):
"""Handle dark mode toggle button click"""
if not await self._check_author(interaction):
return
self.is_dark = not self.is_dark
self.tweet_data["dark"] = self.is_dark
await self.regenerate_tweet(interaction)
async def toggle_verified_callback(self, interaction: discord.Interaction):
"""Handle verified toggle button click"""
if not await self._check_author(interaction):
return
self.is_verified = not self.is_verified
self.tweet_data["verified"] = self.is_verified
await self.regenerate_tweet(interaction)
async def on_timeout(self):
"""Disable buttons when view times out"""
for item in self.children:
item.disabled = True
def tweety_command():
@commands.hybrid_command(
name="tweety", description="Convert a replied message to a tweet image."
)
@commands.cooldown(1, 10, commands.BucketType.user)
async def tweety(self, context):
if hasattr(context, "interaction") and context.interaction:
view = TweetyHelpView(user_id=context.author.id, bot=self.bot)
embed = view.create_embed()
await context.send(embed=embed, view=view, ephemeral=True)
return
if not context.message.reference or not context.message.reference.message_id:
embed = discord.Embed(
title="Error",
description="You must reply to a message to use this command!",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await context.send(embed=embed)
return
try:
original_message = await context.channel.fetch_message(
context.message.reference.message_id
)
processing_embed = discord.Embed(
title="Tweet Generator (Processing)",
description="<a:mariospin:1423677027013103709> Generating tweet... This may take a moment.",
color=0x7289DA,
)
processing_embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
processing_msg = await context.send(embed=processing_embed)
author = original_message.author
display_name = author.display_name or author.name
username = f"@{author.name}"
avatar_url = (
str(author.avatar.url)
if author.avatar
else str(author.default_avatar.url)
)
message_text = original_message.content
for mention in original_message.mentions:
message_text = message_text.replace(
f"<@{mention.id}>", f"@{mention.name}"
)
message_text = message_text.replace(
f"<@!{mention.id}>", f"@{mention.name}"
)
for role in original_message.role_mentions:
message_text = message_text.replace(f"<@&{role.id}>", f"@{role.name}")
for channel in original_message.channel_mentions:
message_text = message_text.replace(
f"<#{channel.id}>", f"#{channel.name}"
)
if not message_text.strip():
await processing_msg.delete()
embed = discord.Embed(
title="Error",
description="No text found! This command only works with text messages.",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await context.send(embed=embed)
return
if len(message_text) > 300:
await processing_msg.delete()
embed = discord.Embed(
title="Error",
description=f"Message is too long! Maximum 300 characters allowed.\nYour message: {len(message_text)} characters",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await context.send(embed=embed)
return
message_text = break_long_words(message_text, max_word_length=50)
ny_tz = pytz.timezone("America/New_York")
msg_time_ny = original_message.created_at.astimezone(ny_tz)
timestamp = msg_time_ny.strftime("%I:%M %p · %b %d, %Y").replace(" 0", " ")
tweet_data = {
"name": display_name[:50],
"handle": username[:20],
"text": message_text[:300],
"avatar": avatar_url,
"timestamp": timestamp,
"verified": False,
"dark": False,
}
API_BASE_URL = "http://tweet.6969.pro"
async with aiohttp.ClientSession() as session:
try:
async with session.post(
f"{API_BASE_URL}/api/render",
json=tweet_data,
headers={"Content-Type": "application/json"},
) as response:
if response.status != 200:
await processing_msg.delete()
error_text = await response.text()
embed = discord.Embed(
title="Error",
description=f"API Error ({response.status}): {error_text}",
color=0xE02B2B,
)
embed.set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
)
await context.send(embed=embed)
return
image_data = await response.read()
with tempfile.NamedTemporaryFile(
delete=False, suffix=".png"
) as temp_file:
temp_file.write(image_data)
temp_file_path = temp_file.name
await processing_msg.delete()
with open(temp_file_path, "rb") as f:
file = discord.File(
f,
filename=f"tweet_{author.name}_{int(datetime.now().timestamp())}.png",
)
embed = discord.Embed(
title="Tweet Generated",
description="<:error:1424007141768822824> Tweet sometimes may look a bit broken, im gonna rewrite the API another time... (it wasnt made for Syntrel in the first place)",
color=0x7289DA,
)
embed.set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
)
embed.set_footer(
text=f"Requested by {context.author.name}",
icon_url=context.author.display_avatar.url,
)
view = TweetyView(
author_id=context.author.id,
original_message=original_message,
tweet_data=tweet_data,
api_url=API_BASE_URL,
)
await context.send(embed=embed)
image_message = await context.send(file=file, view=view)
view.image_message = image_message
os.remove(temp_file_path)
except aiohttp.ClientError:
await processing_msg.delete()
embed = discord.Embed(
title="Error",
description="Connection error: Could not reach tweet API",
color=0xE02B2B,
)
embed.set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
)
await context.send(embed=embed)
except Exception:
await processing_msg.delete()
embed = discord.Embed(
title="Error",
description="Error generating tweet image",
color=0xE02B2B,
)
embed.set_author(
name="Media",
icon_url="https://yes.nighty.works/raw/y5SEZ9.webp",
)
await context.send(embed=embed)
except Exception:
embed = discord.Embed(
title="Error",
description="Error processing the message!",
color=0xE02B2B,
)
embed.set_author(
name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp"
)
await context.send(embed=embed)
return tweety

176
cogs/melonx/__init__.py Normal file
View File

@@ -0,0 +1,176 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .melonx import MelonxView
from .transfer import transfer_command
from .mods import mods_command
from .legal import legal_command
from .gamecrash import crash_command
from .requirements import requirements_command
from .error import error_command
from .ios26 import ios26_command
from .upgrade import upgrade_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Melonx(commands.GroupCog, name="melonx"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="melonx", invoke_without_command=True)
async def melonx_group(self, context: Context):
embed = discord.Embed(
title="MeloNX Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x963155,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
view = MelonxView(self.bot)
await context.send(embed=embed, view=view)
@melonx_group.command(name="help")
async def melonx_group_help(self, context: Context):
embed = discord.Embed(
title="MeloNX Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x963155,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
view = MelonxView(self.bot)
await context.send(embed=embed, view=view)
@melonx_group.command(name="transfer")
async def melonx_group_transfer(self, context: Context):
await self._invoke_hybrid(context, "transfer")
@melonx_group.command(name="legal")
async def melonx_group_legal(self, context: Context):
await self._invoke_hybrid(context, "legal")
@melonx_group.command(name="mods")
async def melonx_group_mods(self, context: Context):
await self._invoke_hybrid(context, "mods")
@melonx_group.command(name="gamecrash")
async def melonx_group_gamecrash(self, context: Context):
await self._invoke_hybrid(context, "gamecrash")
@melonx_group.command(name="requirements")
async def melonx_group_requirements(self, context: Context):
await self._invoke_hybrid(context, "requirements")
@melonx_group.command(name="error")
async def melonx_group_error(self, context: Context):
await self._invoke_hybrid(context, "error")
@melonx_group.command(name="26")
async def melonx_group_26(self, context: Context):
await self._invoke_hybrid(context, "26")
@melonx_group.command(name="upgrade")
async def melonx_group_upgrade(self, context: Context):
await self._invoke_hybrid(context, "upgrade")
async def _invoke_hybrid(self, context: Context, name: str):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command)
else:
await context.send(f"Unknown MeloNX command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@app_commands.command(name="help", description="MeloNX troubleshooting help")
async def help(self, interaction: discord.Interaction):
embed = discord.Embed(
title="MeloNX Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x963155,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
view = MelonxView(self.bot)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="transfer",
description="How to transfer save files from other emulators or platforms",
)
async def transfer(self, context):
return await transfer_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="mods", description="How to install mods within MeloNX (Limited Support)"
)
async def mods(self, context):
return await mods_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="legal", description="Legality of emulators")
async def legal(self, context):
return await legal_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="gamecrash", description="Why does my game crash?")
async def gamecrash(self, context):
return await crash_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="requirements", description="What does MeloNX require?"
)
async def requirements(self, context):
return await requirements_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="error", description="What does this error message mean?"
)
async def error(self, context):
return await error_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="26", description="How can I run MeloNX on iOS 26?")
async def ios26(self, context):
return await ios26_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="upgrade", description="How can I upgrade my firmware and keys in MeloNX?"
)
async def upgrade(self, context):
return await upgrade_command()(self, context)
async def setup(bot) -> None:
cog = Melonx(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'melonx.help'")
bot.logger.info("Loaded extension 'melonx.transfer'")
bot.logger.info("Loaded extension 'melonx.mods'")
bot.logger.info("Loaded extension 'melonx.gamecrash'")
bot.logger.info("Loaded extension 'melonx.requirements'")
bot.logger.info("Loaded extension 'melonx.error'")
bot.logger.info("Loaded extension 'melonx.26'")
bot.logger.info("Loaded extension 'melonx.upgrade'")
bot.logger.info("Loaded extension 'melonx.legal'")

49
cogs/melonx/error.py Normal file
View File

@@ -0,0 +1,49 @@
import discord
from discord.ext import commands
def error_command():
@commands.hybrid_command(
name="error", description="What does this error message mean?"
)
async def error(self, context):
embed = discord.Embed(
color=0x963155,
description=(
"# What does this error message mean?\n\n---\n\n"
+ '**1. "MeloNX Crashed! System.SystemException: Cannot allocate memory"**'
+ "You likely don't have the increased memory limit entitlement enabled, are using an a12 chipset, and have 4GB or less of memory. You can see the status of the entitlement for MeloNX under the Settings tab.\n\n"
+ '**2. "MeloNX Crashed! LibHac.Common.HorizonResultException: ResultLoaderInvalidNso (2009-0005)"**'
+ "This is likely a bad game / update / or DLC dump. redump your files and try again."
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/error.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return error

52
cogs/melonx/gamecrash.py Normal file
View File

@@ -0,0 +1,52 @@
import discord
from discord.ext import commands
def crash_command():
@commands.hybrid_command(name="gamecrash", description="Why does my game crash?")
async def crash(self, context):
embed = discord.Embed(
color=0x963155,
description=(
'# "Why does my game crash?"\n\n---\n\n'
+ "**This can be caused by multiple reasons:**\n"
+ "- Not enough available ram\n"
+ "- You may have tried to update your firmware without updating your keys\n"
+ "- Game file is corrupted/broken\n"
+ "- Game requires a higher firmware+keys combination than you currently have\n"
+ "- In rare cases some games also crash when not having the resolution set to 1x\n"
+ "- The a12 chipset have a lot of issues at the moment\n"
+ "- Shader cache isn't cleared after updating MeloNX\n"
+ "- iOS 15 and 16 are known to have some compatibility issues"
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/gamecrash.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return crash

52
cogs/melonx/ios26.py Normal file
View File

@@ -0,0 +1,52 @@
import discord
from discord.ext import commands
def ios26_command():
@commands.hybrid_command(name="26", description="How can I run MeloNX on iOS 26?")
async def ios26(self, context):
embed = discord.Embed(
color=0x963155,
description=(
'# "How can I run MeloNX on iOS 26?"\n\n---\n\n'
+ "### StikDebug\n"
+ "1. Make sure StikDebug is on the latest version.\n"
+ '2. Turn on "Picture in Picture" in StikDebug\'s settings.\n\n'
+ "### MeloNX\n"
+ "Make sure you're on the latest version of MeloNX.\n"
+ "## Disclaimer:\n\n"
+ "If you're on iOS 18 or below, and emulation is essential to you:\n\n"
+ "## <:error:1424007141768822824> DO NOT UPDATE <:error:1424007141768822824> \n\n"
+ "iOS 26 has many issues related to emulation."
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/ios26.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return ios26

64
cogs/melonx/legal.py Normal file
View File

@@ -0,0 +1,64 @@
import discord
from discord.ext import commands
def legal_command():
@commands.hybrid_command(name="legal", description="Legality of emulators.")
async def legal(self, context):
embed = discord.Embed(
color=0x963155,
description=(
"# Legality\n\n"
+ "---\n\n"
+ "## Overview\n"
+ "Emulators themselves are **legal**, as long as you use them with **legally dumped copies** of games **you** own, "
+ "or with **homebrew software**.\n\n"
+ "Read about the landmark case [**Sony v. Bleem!** (2000)](https://www.copyright.gov/fair-use/summaries/sonycomputer-bleem-9thcir2000.pdf), "
+ "which helped set the precedent for emulation being legal. You can also watch "
+ "[this video](https://www.youtube.com/watch?v=yj9Gk84jRiE) for more information.\n\n"
"## Legal Basis\n"
+ "According to the [**U.S. Copyright Act**](https://www.copyright.gov/title17/92chap1.html#117) "
+ "(the law under which Discord operates), you **must own a legal copy** of any game you play on **MeloNX**.\n\n"
+ "- Downloading games you do not own is considered **piracy** and is **illegal**.\n"
+ "- Even if another copy is identical, it still belongs to someone else, you are **not entitled** to it.\n\n"
+ "## Our Stance on Piracy\n"
+ "We **do not support piracy**. Doing so would give Nintendo legal grounds to take us down.\n"
+ "And yes — **Nintendo is aware of MeloNX's existence.**\n\n"
+ "We are not required to monitor user behavior, but we **strongly encourage** everyone to use only **legally obtained copies** of games.\n\n"
+ "## Enforcement\n"
+ "If you are found using pirated games with MeloNX, you will be **banned** from this Discord server **without warning**.\n\n"
+ "## Final Note\n"
+ "Thank you for understanding and respecting the hard work that went into creating both the emulator MeloNX is built on, "
+ "and **MeloNX itself.**"
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by stossy11")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/legal.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return legal

183
cogs/melonx/melonx.py Normal file
View File

@@ -0,0 +1,183 @@
import discord
from discord.ext import commands
class MelonxSelect(discord.ui.Select):
def __init__(self, bot):
self.bot = bot
options = [
discord.SelectOption(
label="Transfer",
value="transfer",
description="How to transfer save files from other emulators or platforms",
),
discord.SelectOption(
label="Mods",
value="mods",
description="How to install mods within MeloNX (Limited Support)",
),
discord.SelectOption(
label="Game Crash",
value="gamecrash",
description="Why does my game crash?",
),
discord.SelectOption(
label="Requirements",
value="requirements",
description="What does MeloNX require?",
),
discord.SelectOption(
label="Error",
value="error",
description="What does this error message mean?",
),
discord.SelectOption(
label="iOS 26",
value="26",
description="How can I run MeloNX on iOS 26?",
),
discord.SelectOption(
label="Upgrade",
value="upgrade",
description="How can I upgrade my firmware and keys in MeloNX?",
),
discord.SelectOption(
label="Legal",
value="legal",
description="Legality of emulators and our stance on piracy",
),
]
super().__init__(placeholder="Choose a MeloNX command...", options=options)
async def callback(self, interaction: discord.Interaction):
command_name = self.values[0]
command = self.bot.get_command(command_name)
if command:
try:
ctx = await self.bot.get_context(interaction.message)
if ctx:
await ctx.invoke(command)
embed = discord.Embed(
title="Command Executed",
description=f"Successfully executed `/{command_name}`",
color=0x00FF00,
)
embed.set_author(
name="MeloNX",
icon_url="https://yes.nighty.works/raw/TLGaVa.png",
)
await interaction.response.edit_message(embed=embed, view=None)
except discord.Forbidden:
guild_info = (
f"server {interaction.guild.name} (ID: {interaction.guild.id})"
if interaction.guild
else "DM or private channel"
)
self.bot.logger.warning(
f"Bot missing permissions in {guild_info} - cannot execute {command_name} command"
)
if interaction.guild is None:
embed = discord.Embed(
title="Error",
description="This command cannot be executed in DMs.",
color=0xFF0000,
)
else:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xFF0000,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
await interaction.response.edit_message(embed=embed, view=None)
except Exception as e:
self.bot.logger.error(f"Error executing {command_name} command: {e}")
embed = discord.Embed(
title="Error",
description="An error occurred while executing the command.",
color=0x963155,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
await interaction.response.edit_message(embed=embed, view=None)
else:
embed = discord.Embed(
title="Error", description="Command not found!", color=0x963155
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
await interaction.response.edit_message(embed=embed, view=None)
class MelonxView(discord.ui.View):
def __init__(self, bot):
super().__init__()
self.add_item(MelonxSelect(bot))
def melonx_command():
@commands.hybrid_command(
name="melonx", description="MeloNX troubleshooting and help"
)
async def melonx(self, context):
if isinstance(context.channel, discord.DMChannel):
embed = discord.Embed(
title="Error",
description="This command can only be used in servers.",
color=0xE02B2B,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
await context.send(embed=embed, ephemeral=True)
return
if isinstance(context.channel, discord.PartialMessageable):
embed = discord.Embed(
title="Error",
description="The bot needs send messages permissions in this channel.",
color=0xE02B2B,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
await context.send(embed=embed, ephemeral=True)
return
embed = discord.Embed(
title="MeloNX Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x963155,
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
view = MelonxView(self.bot)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, view=view, ephemeral=True
)
else:
await context.send(embed=embed, view=view)
return melonx

55
cogs/melonx/mods.py Normal file
View File

@@ -0,0 +1,55 @@
import discord
from discord.ext import commands
def mods_command():
@commands.hybrid_command(
name="mods", description="How to install mods within MeloNX (Limited Support)"
)
async def mods(self, context):
embed = discord.Embed(
color=0x963155,
description=(
"# How do I install mods within MeloNX? (Limited Support)\n\n---\n\n"
+ "### **romFS/exeFS mods**:\n"
+ "1. Obtain your title ID of your game by copying it from MeloNX, Hold down on the game and click game info.\n"
+ "2. Copy it and then go to Files-> MeloNX-> mods-> contents\n"
+ "3. In the contents folder create a new folder and name it the title ID you copied earlier.\n"
+ "4. Now place all your mods for that game in the folder you just made (these should be folders with the mod name, do not mess with the file structure of the mod after unzipping it.)\n\n"
+ "### **Atmosphere mods**: \n"
+ "1. Obtain your title ID of your game by copying it from MeloNX, Hold down on the game and click game info.\n"
+ "2. Copy it and then go to Files-> MeloNX-> sdcard-> atmosphere-> contents\n"
+ "3. In the contents folder create a new folder and name it the title ID you copied earlier.\n"
+ "4. Now place all your mods for that game in the folder you just made (these should be folders with the mod name, do not mess with the file structure of the mod after unzipping it.)"
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/mods.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return mods

View File

@@ -0,0 +1,50 @@
import discord
from discord.ext import commands
def requirements_command():
@commands.hybrid_command(
name="requirements", description="What does MeloNX require?"
)
async def requirements(self, context):
embed = discord.Embed(
color=0x963155,
description=(
'# "What does MeloNX require?"\n\n---\n\n'
+ "- JIT is **Mandatory**, because of this MeloNX will never be on the App Store / TestFlight\n"
+ "- A Modded Nintendo Switch\n"
+ "- The Increased Memory Limit Entitlement\n"
+ "- A device with a **A12/M1** chip and **4GB Ram** or higher\n"
+ "- TrollStore is supported with limited functionality for iOS 15"
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/requirements.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return requirements

61
cogs/melonx/transfer.py Normal file
View File

@@ -0,0 +1,61 @@
import discord
from discord.ext import commands
def transfer_command():
@commands.hybrid_command(
name="transfer",
description="How to transfer save files from other emulators or platforms",
)
async def transfer(self, context):
embed = discord.Embed(
color=0x963155,
description=(
"# How do I transfer my save files from another emulator, my pc, or other platform?\n\n---\n\n"
+ "### **Ryujinx Based**: \n"
+ '1. Go too Ryujinx\'s main directory -> copy and and transfer the **"bis"** folder to your iDevice.\n'
+ '2. Replace the **"bis"** folder in MeloNX with the one you transferred over.\n\n'
+ "### **Yuzu Based**:\n"
+ '1. Go to Yuzu\'s main directory and locate "**nand**" then go to -> users -> save\n'
+ '2. Get the **title ID** of the game you want to transfer to by locating the game on MeloNX, hold down on it and press "Game Info" \n'
+ "3. Then search the title ID within the **save** folder\n"
+ "4. Open that folder labeled your title ID and copy of all of the contents to your iDevice\n"
+ "5. Boot the game once in MeloNX so the save directories appear and is the latest created\n"
+ "6. On your iDevice, go into files-> MeloNX -> bis -> user -> save\n"
+ "7. In the save folder, there will be many folders named 0000001, 0000002, etc\n"
+ "8. Sort by last modified or open each one too see the modified date/time of the files\n"
+ "9. Once you've found the newest one, inside will be two folders named 1 and 0, one will have a brand new save file inside.\n"
+ '10. drop the contents copied from the title ID folder in Yuzu into that directory and press "replace" when prompted.\n'
+ "11. Launch the game again in MeloNX to verify the transfer."
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/transfer.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return transfer

59
cogs/melonx/upgrade.py Normal file
View File

@@ -0,0 +1,59 @@
import discord
from discord.ext import commands
def upgrade_command():
@commands.hybrid_command(
name="upgrade", description="How can I upgrade my firmware and keys in MeloNX?"
)
async def upgrade(self, context):
embed = discord.Embed(
color=0x963155,
description=(
"# How can I upgrade my firmware and keys in MeloNX?\n\n---\n\n"
+ "First, dump **BOTH** the firmware and keys from a **MODDED SWITCH**.\n\n"
+ "## Keys:\n"
+ "1. Go to the root folder of MeloNX in the files app.\n"
+ '2. Open the "system" folder.\n'
+ '3. Delete the "prod.keys" and "title.keys".\n\n'
+ "## Firmware:\n"
+ "1. Go to the root folder of MeloNX in the files app.\n"
+ '2. Open the "bis" folder.\n'
+ '3. Open the "system" folder.\n'
+ '4. Delete the "Contents" folder.\n\n'
+ "## Lastly:\n"
+ "1. Go to MeloNX's advanced tab in settings.\n"
+ '2. Select the "Show Setup Screen" option.\n\n'
+ "You will now be able to import your new keys and firmware."
),
)
embed.set_author(
name="MeloNX", icon_url="https://yes.nighty.works/raw/TLGaVa.png"
)
embed.set_footer(text="Last Edited by Meshal :D")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/melonx/upgrade.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="MeloNX Discord",
style=discord.ButtonStyle.primary,
url="https://discord.gg/EMXB2XYQgA",
emoji="<:Discord:1428762057758474280>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return upgrade

View File

@@ -0,0 +1,207 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .dontasktoask import dontasktoask_command
from .rickroll import rr_command
from .depart import depart_command
from .labubu import labubu_command
from .duck import duck_command
from .tryitandsee import tryitandsee_command
from .piracy import piracy_command
from .keanu import keanu_command
from .support import support_command
from .docs import docs_command
from .sigma import sigma_command
from .silly import silly_command
from .color import color_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Miscellaneous(commands.GroupCog, name="misc"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="miscellaneous", aliases=["misc"], invoke_without_command=True)
async def miscellaneous_group(self, context: Context):
embed = discord.Embed(
title="Miscellaneous Commands",
description="Use `.misc <subcommand>` or `/misc <subcommand>`.",
color=0x7289DA,
)
embed.set_author(
name="Miscellaneous", icon_url="https://yes.nighty.works/raw/YxMC0r.png"
)
embed.add_field(
name="Available",
value="dontasktoask, rr, depart, labubu, duck, tryitandsee, piracy, keanu, support, docs, sigma, silly, color",
inline=False,
)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command)
else:
await context.send(f"Unknown miscellaneous command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@miscellaneous_group.command(name="dontasktoask")
async def miscellaneous_group_dontasktoask(self, context: Context):
await self._invoke_hybrid(context, "dontasktoask")
@miscellaneous_group.command(name="rr")
async def miscellaneous_group_rr(self, context: Context):
await self._invoke_hybrid(context, "rr")
@miscellaneous_group.command(name="depart")
async def miscellaneous_group_depart(self, context: Context):
await self._invoke_hybrid(context, "depart")
@miscellaneous_group.command(name="labubu")
async def miscellaneous_group_labubu(self, context: Context):
await self._invoke_hybrid(context, "labubu")
@miscellaneous_group.command(name="duck")
async def miscellaneous_group_duck(self, context: Context):
await self._invoke_hybrid(context, "duck")
@miscellaneous_group.command(name="tryitandsee")
async def miscellaneous_group_tryitandsee(self, context: Context):
await self._invoke_hybrid(context, "tryitandsee")
@miscellaneous_group.command(name="piracy")
async def miscellaneous_group_piracy(self, context: Context):
await self._invoke_hybrid(context, "piracy")
@miscellaneous_group.command(name="keanu")
async def miscellaneous_group_keanu(self, context: Context):
await self._invoke_hybrid(context, "keanu")
@miscellaneous_group.command(name="support")
async def miscellaneous_group_support(self, context: Context):
await self._invoke_hybrid(context, "support")
@miscellaneous_group.command(name="docs")
async def miscellaneous_group_docs(self, context: Context):
await self._invoke_hybrid(context, "docs")
@miscellaneous_group.command(name="sigma")
async def miscellaneous_group_sigma(self, context: Context):
await self._invoke_hybrid(context, "sigma")
@miscellaneous_group.command(name="silly")
async def miscellaneous_group_silly(
self, context: Context, message_type: str = "regular"
):
await self._invoke_hybrid(context, "silly", message_type=message_type)
@miscellaneous_group.command(name="color")
async def miscellaneous_group_color(self, context: Context):
await self._invoke_hybrid(context, "color")
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="dontasktoask", description="Shows the 'Don't Ask to Ask' image."
)
async def dontasktoask(self, context):
return await dontasktoask_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="rr", description="Rickroll")
async def rr(self, context):
return await rr_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="depart", description="Show the departure meme")
async def depart(self, context):
return await depart_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="labubu", description="Labubu ASCII art")
async def labubu(self, context):
return await labubu_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="duck", description="Duck ASCII art")
async def duck(self, context):
return await duck_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="tryitandsee", description="Try it and see")
async def tryitandsee(self, context):
return await tryitandsee_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="piracy", description="FBI Anti Piracy Warning")
async def piracy(self, context):
return await piracy_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="keanu", description="Reeves")
async def keanu(self, context):
return await keanu_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="support", description="Support?")
async def support(self, context):
return await support_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="docs", description="Shows the docs image.")
async def docs(self, context):
return await docs_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="sigma", description="i feel so sigma!")
async def sigma(self, context):
return await sigma_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="silly", description="Sends a silly message :3")
@app_commands.describe(message_type="Type of message to send (regular or animated)")
@app_commands.choices(
message_type=[
app_commands.Choice(name="Regular", value="regular"),
app_commands.Choice(name="Animated", value="animated"),
]
)
async def silly(self, context, message_type: str = "regular"):
return await silly_command()(self, context, message_type=message_type)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="color", description="Get a random color.")
async def color(self, context):
return await color_command()(self, context)
async def setup(bot) -> None:
cog = Miscellaneous(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'miscellaneous.dontasktoask'")
bot.logger.info("Loaded extension 'miscellaneous.rr'")
bot.logger.info("Loaded extension 'miscellaneous.depart'")
bot.logger.info("Loaded extension 'miscellaneous.labubu'")
bot.logger.info("Loaded extension 'miscellaneous.duck'")
bot.logger.info("Loaded extension 'miscellaneous.tryitandsee'")
bot.logger.info("Loaded extension 'miscellaneous.piracy'")
bot.logger.info("Loaded extension 'miscellaneous.keanu'")
bot.logger.info("Loaded extension 'miscellaneous.support'")
bot.logger.info("Loaded extension 'miscellaneous.docs'")
bot.logger.info("Loaded extension 'miscellaneous.sigma'")
bot.logger.info("Loaded extension 'miscellaneous.silly'")
bot.logger.info("Loaded extension 'miscellaneous.color'")

View File

@@ -0,0 +1,52 @@
import discord
from discord.ext import commands
import random
import colorsys
def color_command():
@commands.hybrid_command(name="color", description="Get a random color.")
async def color(self, context):
"""
Generates a random color and displays information about it.
"""
random_color_int = random.randint(0, 0xFFFFFF)
color = discord.Color(random_color_int)
r, g, b = color.r, color.g, color.b
rgb_decimal = (r / 255, g / 255, b / 255)
h, l, s = colorsys.rgb_to_hls(rgb_decimal[0], rgb_decimal[1], rgb_decimal[2])
h_hsv, s_hsv, v_hsv = colorsys.rgb_to_hsv(
rgb_decimal[0], rgb_decimal[1], rgb_decimal[2]
)
embed = discord.Embed(color=color)
embed.set_author(
name="Random Color", icon_url="https://yes.nighty.works/raw/YxMC0r.png"
)
embed.add_field(name="Hex", value=str(color))
embed.add_field(name="RGB", value=f"rgb({r}, {g}, {b})")
embed.add_field(
name="RGB Decimal",
value=f"{rgb_decimal[0]:.3f}, {rgb_decimal[1]:.3f}, {rgb_decimal[2]:.3f}",
)
embed.add_field(
name="HSL", value=f"hsl({h * 360:.0f}, {s * 100:.0f}%, {l * 100:.0f}%)"
)
embed.add_field(
name="HSV",
value=f"hsv({h_hsv * 360:.0f}, {s_hsv * 100:.0f}%, {v_hsv * 100:.0f}%)",
)
embed.add_field(name="Integer", value=str(random_color_int))
embed.set_thumbnail(
url=f"https://singlecolorimage.com/get/{str(color).replace('#', '')}/150x150"
)
await context.send(embed=embed)
return color

View File

@@ -0,0 +1,29 @@
import discord
from discord.ext import commands
import aiohttp
import io
def depart_command():
@commands.hybrid_command(
name="depart",
description="Show the departure meme",
)
async def depart(self, context):
gif_url = "https://yes.nighty.works/raw/Mp6YGV.gif"
async with aiohttp.ClientSession() as session:
async with session.get(gif_url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="depart.gif")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(file=file, ephemeral=False)
else:
await inter.followup.send(file=file, ephemeral=True)
else:
await context.send(file=file)
return depart

View File

@@ -0,0 +1,32 @@
import discord
from discord.ext import commands
import aiohttp
import io
def docs_command():
@commands.hybrid_command(name="docs", description="Shows the docs image.")
async def docs(self, context):
url = "https://yes.nighty.works/raw/akdx0q.webp"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="docs.webp")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(
"## Dont be like the rest, read the docs",
file=file,
ephemeral=False,
)
else:
await inter.followup.send(
"## Dont be like the rest, read the docs", file=file, ephemeral=True
)
else:
await context.send("## Dont be like the rest, read the docs", file=file)
return docs

View File

@@ -0,0 +1,28 @@
import discord
from discord.ext import commands
import aiohttp
import io
def dontasktoask_command():
@commands.hybrid_command(
name="dontasktoask", description="Shows the 'Don't Ask to Ask' image."
)
async def dontasktoask(self, context):
image_url = "https://yes.nighty.works/raw/KecbCr.jpg"
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="dontasktoask.jpg")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(file=file, ephemeral=False)
else:
await inter.followup.send(file=file, ephemeral=True)
else:
await context.send(file=file)
return dontasktoask

View File

@@ -0,0 +1,36 @@
from discord.ext import commands
def duck_command():
@commands.hybrid_command(
name="duck",
description="Duck ASCII art",
)
async def duck(self, context):
duck_art = """
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⣉⡥⠶⢶⣿⣿⣿⣿⣷⣆⠉⠛⠿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡿⢡⡞⠁⠤⠈⠿⠿⠿⠿⣿⢻⣦⡈⠻⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡇⠘⡁⠀⢀⣀⣀⣀⣈⣁⣐⡒⠢⢤⡈⠛⢿⡄⠻⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡇⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠉⠐⠄⡈⢀⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⠇⢠⣿⣿⣿⣿⡿⢿⣿⣿⣿⠁⢈⣿⡄⢀⣀⠸⣿⣿⣿⣿
⣿⣿⣿⣿⡿⠟⣡⣶⣶⣬⣭⣥⣴⣾⣿⣿⣿⣶⣾⣿⣧⣼⣿⣷⣌⡻⢿⣿
⣿⣿⠟⣋⣴⣾⣿⣿⣿⣿⣿⣿⣿⡇⢿⣿⣿⣿⣿⣿⣿⡿⢸⣿⣿⣿⣿⣷⠄⢻
⠰⢾⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⢂⣭⣿⣿⣿⣿⣿⠇⠘⠛⠛⢉⣉⣠⣴⣾
⣿⣷⣦⣬⣍⣉⣉⣛⣛⣉⠉⣤⣶⣾⣿⣿⣿⣿⣿⣿⡿⢰⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡘⣿⣿⣿⣿⣿⣿⣿⣿⡇⣼⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⢸⣿⣿⣿⣿⣿⣿⣿⠁⣿⣿⣿⣿⣿⣿⣿⣿⣿
"""
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(
f"```ansi\n{duck_art}\n```", ephemeral=False
)
else:
await inter.followup.send(f"```ansi\n{duck_art}\n```", ephemeral=True)
else:
await context.send(f"```ansi\n{duck_art}\n```")
return duck

View File

@@ -0,0 +1,50 @@
import discord
from discord.ext import commands
import random
def keanu_command():
@commands.hybrid_command(
name="keanu",
description="Reeves",
)
async def keanu(self, context):
images = [
"https://yes.nighty.works/raw/z0HqUM.png",
"https://yes.nighty.works/raw/1Jc0j6.avif",
"https://yes.nighty.works/raw/uQyDyg.webp",
"https://yes.nighty.works/raw/LzrPZz.png",
"https://yes.nighty.works/raw/BZgKzR.jpg",
"https://yes.nighty.works/raw/xOzCta.jpg",
"https://yes.nighty.works/raw/eWvQa5.webp",
"https://yes.nighty.works/raw/Qg9HJr.webp",
"https://yes.nighty.works/raw/tYfOEn.webp",
"https://yes.nighty.works/raw/kZS1Mu.jpg",
"https://yes.nighty.works/raw/E83And.png",
"https://yes.nighty.works/raw/PRr6ln.jpg",
"https://yes.nighty.works/raw/ZlprB5.jpg",
"https://yes.nighty.works/raw/BvcXOg.jpg",
"https://yes.nighty.works/raw/C7gy4v.jpg",
"https://yes.nighty.works/raw/XqHg1q.jpg",
"https://yes.nighty.works/raw/RUXNK7.png",
"https://yes.nighty.works/raw/CBNs9L.jpg",
]
embed = discord.Embed(
description="## Reeves",
color=0x7289DA,
)
embed.set_author(
name="Keanu", icon_url="https://yes.nighty.works/raw/YxMC0r.png"
)
embed.set_image(url=random.choice(images))
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(embed=embed, ephemeral=False)
else:
await inter.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed)
return keanu

View File

@@ -0,0 +1,65 @@
import discord
from discord.ext import commands
def labubu_command():
@commands.hybrid_command(
name="labubu",
description="Labubu ASCII art",
)
async def labubu(self, context):
labubu_art = """⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠗⠀⠀⣀⣄⠀⢿⣿⣿⣿⠟⠁⢠⡆⠉⠙⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⠀⣴⣿⡟⠀⠘⣿⣿⠋⠀⠀⠀⢠⣶⡀⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⢠⣿⠛⣶⠀⠀⣿⡟⠀⠀⠀⢠⣿⣿⡇⠀⠠⣽⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠅⠀⠀⣿⠏⠀⣿⠀⠀⣿⠁⠀⠀⢠⣿⠟⢻⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⣼⣿⠀⢰⡟⠀⠀⠛⠀⠀⠀⣾⡇⠀⢸⡇⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠿⠃⠀⠈⠀⢀⠀⣀⣀⠀⠘⠟⠀⠀⡾⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⢀⠂⠀⠈⠉⢴⣽⣿⠵⣿⡶⣂⣄⡀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡟⠡⠆⢀⠀⠀⠀⠀⠄⠀⠈⠘⠏⢿⣶⣿⡿⢟⣱⣖⠒⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⡟⣻⠏⣠⠄⠀⢀⡀⠀⠀⠀⠀⠈⠀⠀⠀⢸⣿⢦⠄⠙⣿⡇⠩⣭⣅⠈⢿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣟⣼⡇⠈⢀⣴⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠁⠀⢀⠀⠈⠰⣶⡤⠿⠃⢸⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⡟⠉⢠⡶⠋⠀⠀⠀⠀⠀⠀⠀⢀⣤⣤⣴⣶⣤⣄⡀⠀⠀⠂⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿
⣿⣿⣿⡏⢀⡠⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣦⣄⠀⠀⠂⠀⠈⣿⣿⣿⣿
⣿⣿⣿⢃⠈⠀⢠⠀⠀⠀⠀⠀⢠⣿⣿⣿⠿⣩⣏⡙⣛⣛⣿⣿⣿⣿⣿⣿⣿⡿⢇⠀⠀⠄⠀⠘⣿⣿⣿
⣿⣿⣿⡎⠀⠀⠀⠀⠀⠀⠀⠠⣿⣿⣿⡟⣰⣿⠁⢀⠈⢿⣿⣿⣿⣿⢁⣴⠖⢲⣾⡇⠀⠀⠄⠀⣿⣿⣿
⣿⣿⣿⢀⠀⠀⠀⠀⠀⠀⠀⠀⣏⢿⣿⡇⣿⡇⠀⠀⠀⣼⣿⣿⣿⡇⣼⡏⠀⠀⣿⡇⠀⠀⠀⠀⣻⣿⣿
⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⢸⣄⠻⣷⡘⣷⣀⣀⣴⣿⡟⠉⠛⠓⣿⡇⠀⢰⣿⡇⠀⠀⠀⣼⣿⣿⣿
⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠙⢷⣌⠻⢿⣿⣿⣿⣿⣿⣦⣶⣿⣾⣧⣤⡾⠏⠀⠀⠀⠀⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⠶⢌⣉⣛⠛⠿⠿⠿⠿⠿⠛⠉⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣶⣄⠀⠀⠀⠀⠲⠀⠀⠀⠀⠀⠀⠉⠉⠉⠀⠀⠀⠈⠁⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠻⠿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡏⠛⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⡻⣿⣿
⣿⣿⣿⣿⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡛⡛⣁⣿
⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⣿
⣿⣿⣿⣿⠇⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣄⣠⣴⣾⣿⣿⣿⣿
⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⡿⠋⠠⣾⡇⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿
⡟⢁⣠⣤⣦⣌⡃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣏⡹⢿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿
⣿⡿⠿⢶⡬⠙⠟⠋⣁⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣏⠛⠚⠃⠀⠀⢰⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣻⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣧⣆⣤⣄⣤⣼⠁⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⠿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣧⣤⣴⣴⣦⠀⣄⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠐⠚⠛⠓⠂⢀⡄⠀⢰⢽⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣰⣤⣄⣀⢀⠀⢠⡅⠀⠀⢀⣤⣤⡼⠧⣤⣤⣠⣤⣤⣄⡀⡀⣰⣦⣣⠈⡡⣽
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣹⣿⣿⣿⣾⣷⣾⡗⣒⠶⣿⣿⣿⡷⣾⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿"""
embed = discord.Embed(
description=f"```ansi\n{labubu_art}\n```",
color=0x7289DA,
)
embed.set_author(
name="Labubu", icon_url="https://yes.nighty.works/raw/YxMC0r.png"
)
embed.set_footer(text="May look broken on mobile")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(embed=embed, ephemeral=False)
else:
await inter.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed)
return labubu

View File

@@ -0,0 +1,29 @@
import discord
from discord.ext import commands
import aiohttp
import io
def piracy_command():
@commands.hybrid_command(
name="piracy",
description="FBI Anti Piracy Warning",
)
async def piracy(self, context):
image_url = "https://yes.nighty.works/raw/lEhuWK.png"
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="piracy.png")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(file=file, ephemeral=False)
else:
await inter.followup.send(file=file, ephemeral=True)
else:
await context.send(file=file)
return piracy

View File

@@ -0,0 +1,29 @@
import discord
from discord.ext import commands
import aiohttp
import io
def rr_command():
@commands.hybrid_command(
name="rr",
description="Rickroll",
)
async def rr(self, context):
gif_url = "https://yes.nighty.works/raw/JzjMcs.gif"
async with aiohttp.ClientSession() as session:
async with session.get(gif_url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="rickroll.gif")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(file=file, ephemeral=False)
else:
await inter.followup.send(file=file, ephemeral=True)
else:
await context.send(file=file)
return rr

View File

@@ -0,0 +1,29 @@
import discord
from discord.ext import commands
import aiohttp
import io
def sigma_command():
@commands.hybrid_command(
name="sigma",
description="i feel so sigma",
)
async def sigma(self, context):
image_url = "https://yes.nighty.works/raw/EpWodj.jpeg"
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="sigma.png")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(file=file, ephemeral=False)
else:
await inter.followup.send(file=file, ephemeral=True)
else:
await context.send(file=file)
return sigma

View File

@@ -0,0 +1,30 @@
from discord.ext import commands
def silly_command():
@commands.hybrid_command(
name="silly",
description="Sends a silly message :3",
)
async def silly(self, context, message_type: str = "regular"):
if message_type == "animated":
message = "https://yes.nighty.works/raw/LX4nqt.gif"
else:
message = ":3"
interaction = getattr(context, "interaction", None)
if interaction is not None:
await interaction.response.defer(ephemeral=True)
await context.channel.send(message)
try:
await interaction.delete_original_response()
except:
pass
else:
try:
await context.message.delete()
except:
pass
await context.channel.send(message)
return silly

View File

@@ -0,0 +1,26 @@
import discord
from discord.ext import commands
import aiohttp
import io
def support_command():
@commands.hybrid_command(name="support", description="Shows the support image.")
async def support(self, context):
url = "https://yes.nighty.works/raw/wGzHIV.gif"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
data = await resp.read()
file = discord.File(io.BytesIO(data), filename="support.gif")
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(file=file, ephemeral=False)
else:
await inter.followup.send(file=file, ephemeral=True)
else:
await context.send(file=file)
return support

View File

@@ -0,0 +1,21 @@
from discord.ext import commands
def tryitandsee_command():
@commands.hybrid_command(
name="tryitandsee",
description="Try it and see",
)
async def tryitandsee(self, context):
link = "https://tryitands.ee/"
if getattr(context, "interaction", None):
inter = context.interaction
if not inter.response.is_done():
await inter.response.send_message(link, ephemeral=False)
else:
await inter.followup.send(link, ephemeral=True)
else:
await context.send(link)
return tryitandsee

220
cogs/moderation/__init__.py Normal file
View File

@@ -0,0 +1,220 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .ban import ban_command
from .kick import kick_command
from .purge import purge_command
from .warnings import warnings_command
from .archive import archive_command
from .hackban import hackban_command
from .nick import nick_command
from .timeout import timeout_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Moderation(commands.GroupCog, name="moderation"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="moderation", invoke_without_command=True)
async def moderation_group(self, context: Context):
embed = discord.Embed(
title="Moderation Commands",
description="Use `.moderation <subcommand>` or `/moderation <subcommand>`.",
color=0x7289DA,
)
embed.add_field(
name="Available",
value="ban, kick, purge, warnings, archive, hackban, nick, timeout",
inline=False,
)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str, **kwargs):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command, **kwargs)
else:
await context.send(f"Unknown moderation command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
async def timeout_duration_autocomplete(
self,
interaction: discord.Interaction,
current: str,
) -> list[app_commands.Choice[str]]:
options = [
("60 secs", "60s"),
("5 mins", "5m"),
("10 mins", "10m"),
("1 hour", "1h"),
("1 day", "1d"),
("1 week", "1w"),
]
q = (current or "").lower()
results: list[app_commands.Choice[str]] = []
for name, value in options:
if q in name.lower() or q in value.lower():
results.append(app_commands.Choice(name=name, value=value))
return results[:25]
@moderation_group.command(name="ban")
async def moderation_group_ban(
self,
context: Context,
user: discord.User,
*,
reason: str = "Not specified",
delete_messages: str = "none",
):
await self._invoke_hybrid(
context, "ban", user=user, reason=reason, delete_messages=delete_messages
)
@moderation_group.command(name="kick")
async def moderation_group_kick(
self, context: Context, user: discord.User, *, reason: str = "Not specified"
):
await self._invoke_hybrid(context, "kick", user=user, reason=reason)
@moderation_group.command(name="purge")
async def moderation_group_purge(
self, context: Context, amount: int, user: discord.Member = None
):
await self._invoke_hybrid(context, "purge", amount=amount, user=user)
@moderation_group.command(name="warnings")
async def moderation_group_warnings(self, context: Context):
await self._invoke_hybrid(context, "warnings")
@moderation_group.command(name="archive")
async def moderation_group_archive(self, context: Context, limit: int = 10):
await self._invoke_hybrid(context, "archive", limit=limit)
@moderation_group.command(name="hackban")
async def moderation_group_hackban(
self, context: Context, user_id: str, *, reason: str = "Not specified"
):
await self._invoke_hybrid(context, "hackban", user_id=user_id, reason=reason)
@moderation_group.command(name="nick")
async def moderation_group_nick(
self, context: Context, user: discord.User, *, nickname: str = None
):
await self._invoke_hybrid(context, "nick", user=user, nickname=nickname)
@moderation_group.command(name="timeout")
async def moderation_group_timeout(
self,
context: Context,
user: discord.User,
duration: str,
*,
reason: str = "Not specified",
):
await self._invoke_hybrid(
context, "timeout", user=user, duration=duration, reason=reason
)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="ban", description="Bans a user from the server.")
async def ban(
self,
context,
user: discord.User,
*,
reason: str = "Not specified",
delete_messages: str = "none",
):
return await ban_command()(
self, context, user=user, reason=reason, delete_messages=delete_messages
)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="kick", description="Kicks a user from the server.")
async def kick(self, context, user: discord.User, *, reason: str = "Not specified"):
return await kick_command()(self, context, user=user, reason=reason)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="purge", description="Delete a number of messages.")
async def purge(self, context, amount: int, user: discord.Member = None):
return await purge_command()(self, context, amount=amount, user=user)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="warnings", description="Manage warnings of a user on a server."
)
async def warnings(self, context):
return await warnings_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="archive",
description="Archives in a text file the last messages with a chosen limit of messages.",
)
async def archive(self, context, limit: int = 10):
return await archive_command()(self, context, limit=limit)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="hackban",
description="Bans a user without the user having to be in the server.",
)
async def hackban(self, context, user_id: str, *, reason: str = "Not specified"):
return await hackban_command()(self, context, user_id=user_id, reason=reason)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="nick", description="Change the nickname of a user on a server."
)
async def nick(self, context, user: discord.User, *, nickname: str = None):
return await nick_command()(self, context, user=user, nickname=nickname)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="timeout", description="Timeout a user for a specified duration."
)
@app_commands.describe(
user="The user that should be timed out.",
duration="Duration",
reason="The reason why the user should be timed out.",
)
@app_commands.autocomplete(duration=timeout_duration_autocomplete)
async def timeout(
self,
context,
user: discord.User,
duration: str,
*,
reason: str = "Not specified",
):
return await timeout_command()(
self, context, user=user, duration=duration, reason=reason
)
async def setup(bot) -> None:
cog = Moderation(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'moderation.ban'")
bot.logger.info("Loaded extension 'moderation.kick'")
bot.logger.info("Loaded extension 'moderation.purge'")
bot.logger.info("Loaded extension 'moderation.warnings'")
bot.logger.info("Loaded extension 'moderation.archive'")
bot.logger.info("Loaded extension 'moderation.hackban'")
bot.logger.info("Loaded extension 'moderation.nick'")
bot.logger.info("Loaded extension 'moderation.timeout'")

View File

@@ -3,13 +3,9 @@ from datetime import datetime
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Archive(commands.Cog, name="archive"):
def __init__(self, bot) -> None:
self.bot = bot
def archive_command():
@commands.hybrid_command(
name="archive",
description="Archives in a text file the last messages with a chosen limit of messages.",
@@ -18,7 +14,7 @@ class Archive(commands.Cog, name="archive"):
@app_commands.describe(
limit="The limit of messages that should be archived.",
)
async def archive(self, context: Context, limit: int = 10) -> None:
async def archive(self, context, limit: int = 10):
"""
Archives in a text file the last messages with a chosen limit of messages. This command requires the MESSAGE_CONTENT intent to work properly.
@@ -26,7 +22,7 @@ class Archive(commands.Cog, name="archive"):
:param limit: The limit of messages that should be archived. Default is 10.
"""
log_file = f"{context.channel.id}.log"
messages = []
async for message in context.channel.history(
limit=limit, before=context.message
@@ -39,10 +35,10 @@ class Archive(commands.Cog, name="archive"):
if len(attachments) >= 1
else ""
)
message_line = f"{message.created_at.strftime('%m/%d/%Y %H:%M:%S')} {message.author} {message.id}: {message.clean_content} {attachments_text}\n"
messages.append(message_line)
with open(log_file, "w", encoding="UTF-8") as f:
f.write(
f'Archived messages from: #{context.channel} ({context.channel.id}) in the guild "{context.guild}" ({context.guild.id}) at {datetime.now().strftime("%m/%d/%Y %H:%M:%S")}\n'
@@ -50,11 +46,9 @@ class Archive(commands.Cog, name="archive"):
for message_line in reversed(messages):
f.write(message_line)
f = discord.File(log_file)
await context.send(file=f)
os.remove(log_file)
async def setup(bot) -> None:
await bot.add_cog(Archive(bot))
return archive

View File

@@ -1,13 +1,9 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Ban(commands.Cog, name="ban"):
def __init__(self, bot) -> None:
self.bot = bot
def ban_command():
@commands.hybrid_command(
name="ban",
description="Bans a user from the server.",
@@ -17,18 +13,25 @@ class Ban(commands.Cog, name="ban"):
reason="The reason why the user should be banned.",
delete_messages="Delete messages from the user (choose time period).",
)
@app_commands.choices(delete_messages=[
app_commands.Choice(name="Don't delete any messages", value="none"),
app_commands.Choice(name="Last 1 hour", value="1h"),
app_commands.Choice(name="Last 6 hours", value="6h"),
app_commands.Choice(name="Last 12 hours", value="12h"),
app_commands.Choice(name="Last 24 hours", value="1d"),
app_commands.Choice(name="Last 3 days", value="3d"),
app_commands.Choice(name="Last 7 days", value="7d"),
])
@app_commands.choices(
delete_messages=[
app_commands.Choice(name="Don't delete any messages", value="none"),
app_commands.Choice(name="Last 1 hour", value="1h"),
app_commands.Choice(name="Last 6 hours", value="6h"),
app_commands.Choice(name="Last 12 hours", value="12h"),
app_commands.Choice(name="Last 24 hours", value="1d"),
app_commands.Choice(name="Last 3 days", value="3d"),
app_commands.Choice(name="Last 7 days", value="7d"),
]
)
async def ban(
self, context: Context, user: discord.User, *, reason: str = "Not specified", delete_messages: str = "none"
) -> None:
self,
context,
user: discord.User,
*,
reason: str = "Not specified",
delete_messages: str = "none",
):
try:
member = context.guild.get_member(user.id)
if not member:
@@ -41,7 +44,10 @@ class Ban(commands.Cog, name="ban"):
title="Ban",
description=f"**{user}** was banned by **{context.author}**!",
color=0x7289DA,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
return
@@ -50,7 +56,10 @@ class Ban(commands.Cog, name="ban"):
title="Error!",
description="I don't have permission to ban this user.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
except Exception:
@@ -58,41 +67,56 @@ class Ban(commands.Cog, name="ban"):
title="Error!",
description="An error occurred while trying to ban the user.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if not context.author.guild_permissions.ban_members and context.author != context.guild.owner:
if (
not context.author.guild_permissions.ban_members
and context.author != context.guild.owner
):
embed = discord.Embed(
title="Missing Permissions!",
description="You don't have the `Ban Members` permission to use this command.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if member and member.top_role >= context.guild.me.top_role:
embed = discord.Embed(
title="Cannot Ban User",
description="This user has a higher or equal role to me. Make sure my role is above theirs.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if member and context.author != context.guild.owner:
if member.top_role >= context.author.top_role:
embed = discord.Embed(
title="Cannot Ban User",
title="Cannot Ban User",
description="You cannot ban this user as they have a higher or equal role to you.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
delete_message_days = 0
delete_all_messages = False
if delete_messages != "none":
if delete_messages == "all":
delete_all_messages = True
@@ -103,7 +127,7 @@ class Ban(commands.Cog, name="ban"):
elif delete_messages.endswith("d"):
days = int(delete_messages[:-1])
delete_message_days = min(days, 7)
try:
if member:
try:
@@ -111,41 +135,60 @@ class Ban(commands.Cog, name="ban"):
title="Ban",
description=f"You were banned by **{context.author}** from **{context.guild.name}**!\nReason: {reason}",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await member.send(embed=dm_embed)
except (discord.Forbidden, discord.HTTPException):
pass
if member:
await member.ban(reason=reason, delete_message_days=delete_message_days)
await member.ban(
reason=reason, delete_message_days=delete_message_days
)
else:
await context.guild.ban(user, reason=reason, delete_message_days=delete_message_days)
await context.guild.ban(
user, reason=reason, delete_message_days=delete_message_days
)
if delete_all_messages:
await self.delete_all_user_messages(context.guild, user.id)
embed = discord.Embed(
title="Ban",
description=f"**{user}** was banned by **{context.author}**!",
color=0x7289DA,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
embed.add_field(name="Reason:", value=reason)
if delete_messages != "none":
if delete_all_messages:
embed.add_field(name="Messages Deleted:", value="All messages", inline=False)
embed.add_field(
name="Messages Deleted:", value="All messages", inline=False
)
else:
delete_time_text = self.format_delete_time(delete_messages)
embed.add_field(name="Messages Deleted:", value=delete_time_text, inline=False)
embed.add_field(
name="Messages Deleted:",
value=delete_time_text,
inline=False,
)
await context.send(embed=embed)
except discord.Forbidden:
embed = discord.Embed(
title="Error!",
description="I don't have permission to ban this user. Make sure my role is above theirs.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except discord.HTTPException as e:
if "Cannot ban the owner of a guild" in str(e):
@@ -153,37 +196,52 @@ class Ban(commands.Cog, name="ban"):
title="Cannot Ban User",
description="You cannot ban the server owner.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
else:
embed = discord.Embed(
title="Error!",
description=f"Discord API error: {str(e)}",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except Exception as e:
embed = discord.Embed(
title="Debug Error!",
description=f"Error type: {type(e).__name__}\nError message: {str(e)}",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except Exception as e:
except Exception:
embed = discord.Embed(
title="Error!",
description="An unexpected error occurred.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await context.send(embed=embed, ephemeral=True)
async def delete_all_user_messages(self, guild: discord.Guild, user_id: int) -> None:
async def delete_all_user_messages(
self, guild: discord.Guild, user_id: int
) -> None:
for channel in guild.text_channels:
try:
permissions = channel.permissions_for(guild.me)
if not (permissions.read_message_history and permissions.manage_messages):
if not (
permissions.read_message_history and permissions.manage_messages
):
continue
deleted = True
while deleted:
deleted = False
@@ -193,25 +251,27 @@ class Ban(commands.Cog, name="ban"):
try:
await message.delete()
deleted = True
except (discord.NotFound, discord.Forbidden, discord.HTTPException):
except (
discord.NotFound,
discord.Forbidden,
discord.HTTPException,
):
continue
except (discord.Forbidden, discord.HTTPException):
break
except (discord.Forbidden, discord.HTTPException):
continue
def format_delete_time(self, delete_option: str) -> str:
time_formats = {
"1h": "Last 1 hour",
"6h": "Last 6 hours",
"6h": "Last 6 hours",
"12h": "Last 12 hours",
"1d": "Last 24 hours",
"3d": "Last 3 days",
"7d": "Last 7 days"
"7d": "Last 7 days",
}
return time_formats.get(delete_option, "Unknown time period")
async def setup(bot) -> None:
await bot.add_cog(Ban(bot))
return ban

View File

@@ -1,13 +1,9 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class HackBan(commands.Cog, name="hackban"):
def __init__(self, bot) -> None:
self.bot = bot
def hackban_command():
@commands.hybrid_command(
name="hackban",
description="Bans a user without the user having to be in the server.",
@@ -18,9 +14,7 @@ class HackBan(commands.Cog, name="hackban"):
user_id="The user ID that should be banned.",
reason="The reason why the user should be banned.",
)
async def hackban(
self, context: Context, user_id: str, *, reason: str = "Not specified"
) -> None:
async def hackban(self, context, user_id: str, *, reason: str = "Not specified"):
"""
Bans a user without the user having to be in the server.
@@ -37,7 +31,9 @@ class HackBan(commands.Cog, name="hackban"):
title="Ban",
description=f"**{user}** (ID: {user_id}) was banned by **{context.author}**!",
color=0x7289DA,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
except Exception:
@@ -45,9 +41,9 @@ class HackBan(commands.Cog, name="hackban"):
title="Error!",
description="An error occurred while trying to ban the user. Make sure ID is an existing ID that belongs to a user.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(HackBan(bot))
return hackban

View File

@@ -1,13 +1,9 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Kick(commands.Cog, name="kick"):
def __init__(self, bot) -> None:
self.bot = bot
def kick_command():
@commands.hybrid_command(
name="kick",
description="Kicks a user from the server.",
@@ -16,9 +12,7 @@ class Kick(commands.Cog, name="kick"):
user="The user that should be kicked.",
reason="The reason why the user should be kicked.",
)
async def kick(
self, context: Context, user: discord.User, *, reason: str = "Not specified"
) -> None:
async def kick(self, context, user: discord.User, *, reason: str = "Not specified"):
try:
member = context.guild.get_member(user.id)
if not member:
@@ -29,38 +23,53 @@ class Kick(commands.Cog, name="kick"):
title="Error!",
description="This user is not in the server.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if not context.author.guild_permissions.kick_members and context.author != context.guild.owner:
if (
not context.author.guild_permissions.kick_members
and context.author != context.guild.owner
):
embed = discord.Embed(
title="Missing Permissions!",
description="You don't have the `Kick Members` permission to use this command.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if member and member.top_role >= context.guild.me.top_role:
embed = discord.Embed(
title="Cannot Kick User",
description="This user has a higher or equal role to me. Make sure my role is above theirs.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if member and context.author != context.guild.owner:
if member.top_role >= context.author.top_role:
embed = discord.Embed(
title="Cannot Kick User",
title="Cannot Kick User",
description="You cannot kick this user as they have a higher or equal role to you.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
try:
if member:
try:
@@ -69,29 +78,37 @@ class Kick(commands.Cog, name="kick"):
title="Kick",
description=f"You were kicked by **{context.author}** from **{context.guild.name}**!\nReason: {reason}",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
)
except (discord.Forbidden, discord.HTTPException):
pass
await member.kick(reason=reason)
embed = discord.Embed(
title="Kick",
description=f"**{user}** was kicked by **{context.author}**!",
color=0x7289DA,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
except discord.Forbidden:
embed = discord.Embed(
title="Error!",
description="I don't have permission to kick this user. Make sure my role is above theirs.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except discord.HTTPException as e:
if "Cannot kick the owner of a guild" in str(e):
@@ -99,30 +116,39 @@ class Kick(commands.Cog, name="kick"):
title="Cannot Kick User",
description="You cannot kick the server owner.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
else:
embed = discord.Embed(
title="Error!",
description=f"Discord API error: {str(e)}",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except Exception as e:
embed = discord.Embed(
title="Debug Error!",
description=f"Error type: {type(e).__name__}\nError message: {str(e)}",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except Exception as e:
except Exception:
embed = discord.Embed(
title="Error!",
description="An unexpected error occurred.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await context.send(embed=embed, ephemeral=True)
async def setup(bot) -> None:
await bot.add_cog(Kick(bot))
return kick

View File

@@ -1,13 +1,9 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Nick(commands.Cog, name="nick"):
def __init__(self, bot) -> None:
self.bot = bot
def nick_command():
@commands.hybrid_command(
name="nick",
description="Change the nickname of a user on a server.",
@@ -16,9 +12,7 @@ class Nick(commands.Cog, name="nick"):
user="The user that should have a new nickname.",
nickname="The new nickname that should be set.",
)
async def nick(
self, context: Context, user: discord.User, *, nickname: str = None
) -> None:
async def nick(self, context, user: discord.User, *, nickname: str = None):
"""
Change the nickname of a user on a server.
@@ -31,15 +25,19 @@ class Nick(commands.Cog, name="nick"):
title="Missing Permissions!",
description="You are missing the permission(s) `manage_nicknames` to execute this command!",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
return await context.send(embed=embed, ephemeral=True)
if not context.guild.me.guild_permissions.manage_nicknames:
embed = discord.Embed(
title="Missing Permissions!",
description="I am missing the permission(s) `manage_nicknames` to execute this command!",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
return await context.send(embed=embed, ephemeral=True)
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
@@ -51,16 +49,18 @@ class Nick(commands.Cog, name="nick"):
title="Nickname",
description=f"**{member}'s** new nickname is **{nickname}**!",
color=0x7289DA,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await context.send(embed=embed)
except:
embed = discord.Embed(
title="Missing Permissions!",
description="An error occurred while trying to change the nickname of the user. Make sure my role is above the role of the user you want to change the nickname.",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await context.send(embed=embed, ephemeral=True)
async def setup(bot) -> None:
await bot.add_cog(Nick(bot))
return nick

View File

@@ -1,37 +1,50 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Purge(commands.Cog, name="purge"):
def __init__(self, bot) -> None:
self.bot = bot
def purge_command():
@commands.hybrid_command(
name="purge",
description="Delete a number of messages.",
)
@commands.has_guild_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
@app_commands.describe(amount="The amount of messages that should be deleted.")
async def purge(self, context: Context, amount: int) -> None:
"""
Delete a number of messages.
@app_commands.describe(
amount="The amount of messages that should be deleted.",
user="The user whose messages should be deleted (optional).",
)
async def purge(self, context, amount: int, user: discord.Member = None):
if context.interaction:
await context.defer(ephemeral=True)
if user:
deleted_count = 0
def check(message):
nonlocal deleted_count
if message.author == user and deleted_count < amount:
deleted_count += 1
return True
return False
purged_messages = await context.channel.purge(limit=300, check=check)
else:
purged_messages = await context.channel.purge(limit=amount)
:param context: The hybrid command context.
:param amount: The number of messages that should be deleted.
"""
await context.send("Deleting messages...")
purged_messages = await context.channel.purge(limit=amount + 1)
embed = discord.Embed(
title="Purge",
description=f"**{context.author}** cleared **{len(purged_messages)-1}** messages!",
description=f"**{context.author}** cleared **{len(purged_messages)}** messages!"
+ (f" from **{user}**" if user else ""),
color=0x7289DA,
)
embed.set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
await context.channel.send(embed=embed)
embed.set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
if context.interaction:
await context.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, delete_after=10)
async def setup(bot) -> None:
await bot.add_cog(Purge(bot))
return purge

218
cogs/moderation/timeout.py Normal file
View File

@@ -0,0 +1,218 @@
import discord
from discord import app_commands
from discord.ext import commands
from datetime import timedelta
def timeout_command():
DURATION_CHOICES = [
app_commands.Choice(name="60 secs", value="60s"),
app_commands.Choice(name="5 mins", value="5m"),
app_commands.Choice(name="10 mins", value="10m"),
app_commands.Choice(name="1 hour", value="1h"),
app_commands.Choice(name="1 day", value="1d"),
app_commands.Choice(name="1 week", value="1w"),
]
@commands.hybrid_command(
name="timeout",
description="Timeout a user for a specified duration.",
)
@app_commands.describe(
user="The user that should be timed out.",
duration="Duration",
reason="The reason why the user should be timed out.",
)
@app_commands.choices(duration=DURATION_CHOICES)
async def timeout(
self,
context,
user: discord.User,
duration: str,
*,
reason: str = "Not specified",
):
try:
member = context.guild.get_member(user.id)
if not member:
try:
member = await context.guild.fetch_member(user.id)
except discord.NotFound:
embed = discord.Embed(
title="Error!",
description="This user is not in the server.",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if (
not context.author.guild_permissions.moderate_members
and context.author != context.guild.owner
):
embed = discord.Embed(
title="Missing Permissions!",
description="You don't have the `Timeout Members` permission to use this command.",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if member and member.top_role >= context.guild.me.top_role:
embed = discord.Embed(
title="Cannot Timeout User",
description="This user has a higher or equal role to me. Make sure my role is above theirs.",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
if member and context.author != context.guild.owner:
if member.top_role >= context.author.top_role:
embed = discord.Embed(
title="Cannot Timeout User",
description="You cannot timeout this user as they have a higher or equal role to you.",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
seconds = _parse_duration_to_seconds(duration)
if seconds is None or seconds <= 0:
embed = discord.Embed(
title="Invalid Duration",
description="Choose one of: 60 secs, 5 mins, 10 mins, 1 hour, 1 day, 1 week.",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
return
max_seconds = 28 * 24 * 60 * 60
seconds = min(seconds, max_seconds)
try:
timeout_delta = timedelta(seconds=seconds)
await member.timeout(timeout_delta, reason=reason)
embed = discord.Embed(
title="Timeout",
description=f"**{user}** was timed out by **{context.author}**!",
color=0x7289DA,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
embed.add_field(name="Reason:", value=reason)
embed.add_field(
name="Duration:", value=_format_duration(duration), inline=False
)
await context.send(embed=embed)
except discord.Forbidden:
embed = discord.Embed(
title="Error!",
description="I don't have permission to timeout this user. Make sure my role is above theirs.",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except discord.HTTPException as e:
embed = discord.Embed(
title="Error!",
description=f"Discord API error: {str(e)}",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except Exception as e:
embed = discord.Embed(
title="Debug Error!",
description=f"Error type: {type(e).__name__}\nError message: {str(e)}",
color=0xE02B2B,
).set_author(
name="Moderation",
icon_url="https://yes.nighty.works/raw/CPKHQd.png",
)
await context.send(embed=embed, ephemeral=True)
except Exception:
embed = discord.Embed(
title="Error!",
description="An unexpected error occurred.",
color=0xE02B2B,
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await context.send(embed=embed, ephemeral=True)
@timeout.autocomplete("duration")
async def duration_autocomplete(interaction: discord.Interaction, current: str):
query = (current or "").lower()
filtered = [
c
for c in DURATION_CHOICES
if query in c.name.lower() or query in c.value.lower()
]
return filtered[:25]
return timeout
def _parse_duration_to_seconds(duration: str):
try:
s = duration.strip().lower()
if s.endswith("s"):
return int(s[:-1])
if s.endswith("m"):
return int(s[:-1]) * 60
if s.endswith("h"):
return int(s[:-1]) * 60 * 60
if s.endswith("d"):
return int(s[:-1]) * 60 * 60 * 24
if s.endswith("w"):
return int(s[:-1]) * 60 * 60 * 24 * 7
return int(s)
except Exception:
return None
def _format_duration(duration: str) -> str:
mapping = {
"s": "seconds",
"m": "minutes",
"h": "hours",
"d": "days",
"w": "weeks",
}
s = duration.strip().lower()
for suffix, word in mapping.items():
if s.endswith(suffix):
try:
value = int(s[:-1])
return f"{value} {word}"
except Exception:
return duration
try:
value = int(s)
return f"{value} seconds"
except Exception:
return duration

View File

@@ -4,17 +4,18 @@ from discord.ext import commands
from discord.ext.commands import Context
class Warnings(commands.Cog, name="warnings"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(self, context: Context, embed: discord.Embed, *, ephemeral: bool = False) -> None:
def warnings_command():
async def send_embed(
context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(embed=embed, ephemeral=ephemeral)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
@@ -22,7 +23,7 @@ class Warnings(commands.Cog, name="warnings"):
name="warning",
description="Manage warnings of a user on a server.",
)
async def warning(self, context: Context) -> None:
async def warning(self, context) -> None:
"""
Manage warnings of a user on a server.
@@ -33,16 +34,20 @@ class Warnings(commands.Cog, name="warnings"):
title="Missing Permissions!",
description="You are missing the permission(s) `manage_messages` to execute this command!",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
return await self.send_embed(context, embed, ephemeral=True)
if context.invoked_subcommand is None:
embed = discord.Embed(
title="Warning",
description="Please specify a subcommand.\n\n**Subcommands:**\n`add` - Add a warning to a user.\n`remove` - Remove a warning from a user.\n`list` - List all warnings of a user.",
color=0x7289DA,
)
embed.set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
embed.set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await self.send_embed(context, embed)
@warning.command(
@@ -68,7 +73,9 @@ class Warnings(commands.Cog, name="warnings"):
title="Missing Permissions!",
description="You are missing the permission(s) `manage_messages` to execute this command!",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
return await self.send_embed(context, embed, ephemeral=True)
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
@@ -81,7 +88,9 @@ class Warnings(commands.Cog, name="warnings"):
description=f"**{member}** was warned by **{context.author}**!\nTotal warns for this user: {total}",
color=0x7289DA,
)
embed.set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
embed.set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
embed.add_field(name="Reason:", value=reason)
await self.send_embed(context, embed)
try:
@@ -90,7 +99,9 @@ class Warnings(commands.Cog, name="warnings"):
description=f"You were warned by **{context.author}** in **{context.guild.name}**!\nReason: {reason}",
color=0xE02B2B,
)
dm_embed.set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
dm_embed.set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await member.send(embed=dm_embed)
except:
fallback = discord.Embed(
@@ -123,7 +134,9 @@ class Warnings(commands.Cog, name="warnings"):
title="Missing Permissions!",
description="You are missing the permission(s) `manage_messages` to execute this command!",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
return await self.send_embed(context, embed, ephemeral=True)
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
@@ -134,7 +147,9 @@ class Warnings(commands.Cog, name="warnings"):
description=f"Removed the warning **#{warn_id}** from **{member}**!\nTotal warns for this user: {total}",
color=0x7289DA,
)
embed.set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
embed.set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
await self.send_embed(context, embed)
@warning.command(
@@ -154,11 +169,15 @@ class Warnings(commands.Cog, name="warnings"):
title="Missing Permissions!",
description="You are missing the permission(s) `manage_messages` to execute this command!",
color=0xE02B2B,
).set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
).set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
return await self.send_embed(context, embed, ephemeral=True)
warnings_list = await self.bot.database.get_warnings(user.id, context.guild.id)
embed = discord.Embed(title=f"Warnings of {user}", color=0x7289DA)
embed.set_author(name="Moderation", icon_url="https://yes.nighty.works/raw/8VLDcg.webp")
embed.set_author(
name="Moderation", icon_url="https://yes.nighty.works/raw/CPKHQd.png"
)
description = ""
if len(warnings_list) == 0:
description = "This user has no warnings."
@@ -168,7 +187,4 @@ class Warnings(commands.Cog, name="warnings"):
embed.description = description
await self.send_embed(context, embed)
async def setup(bot) -> None:
await bot.add_cog(Warnings(bot))
return warning

View File

@@ -8,13 +8,17 @@ class CogManagement(commands.Cog, name="cog_management"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(self, context: Context, embed: discord.Embed, *, ephemeral: bool = False) -> None:
async def send_embed(
self, context: Context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(embed=embed, ephemeral=ephemeral)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
@@ -22,6 +26,8 @@ class CogManagement(commands.Cog, name="cog_management"):
name="load",
description="Load a cog",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(cog="The name of the cog to load")
@commands.is_owner()
async def load(self, context: Context, cog: str) -> None:
@@ -36,21 +42,28 @@ class CogManagement(commands.Cog, name="cog_management"):
except Exception as e:
embed = discord.Embed(
title="Error",
description=f"Could not load the `{cog}` cog.\n```{str(e)}```", color=0xE02B2B
description=f"Could not load the `{cog}` cog.\n```{str(e)}```",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed, ephemeral=True)
return
embed = discord.Embed(
description=f"Successfully loaded the `{cog}` cog.", color=0x7289DA
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed)
@commands.hybrid_command(
name="unload",
description="Unloads a cog.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(cog="The name of the cog to unload")
@commands.is_owner()
async def unload(self, context: Context, cog: str) -> None:
@@ -65,21 +78,28 @@ class CogManagement(commands.Cog, name="cog_management"):
except Exception as e:
embed = discord.Embed(
title="Error",
description=f"Could not unload the `{cog}` cog.\n```{str(e)}```", color=0xE02B2B
description=f"Could not unload the `{cog}` cog.\n```{str(e)}```",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed, ephemeral=True)
return
embed = discord.Embed(
description=f"Successfully unloaded the `{cog}` cog.", color=0x7289DA
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed)
@commands.hybrid_command(
name="reload",
description="Reloads a cog.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(cog="The name of the cog to reload")
@commands.is_owner()
async def reload(self, context: Context, cog: str) -> None:
@@ -94,16 +114,22 @@ class CogManagement(commands.Cog, name="cog_management"):
except Exception as e:
embed = discord.Embed(
title="Error",
description=f"Could not reload the `{cog}` cog.\n```{str(e)}```", color=0xE02B2B
description=f"Could not reload the `{cog}` cog.\n```{str(e)}```",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed, ephemeral=True)
return
embed = discord.Embed(
title="Cog Management",
description=f"Successfully reloaded the `{cog}` cog.", color=0x7289DA
description=f"Successfully reloaded the `{cog}` cog.",
color=0x7289DA,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed)
async def cog_command_error(self, context: Context, error) -> None:
@@ -111,9 +137,11 @@ class CogManagement(commands.Cog, name="cog_management"):
embed = discord.Embed(
title="Permission Denied",
description="You are not the owner of the bot!",
color=0xE02B2B
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed, ephemeral=True)
else:
raise error

View File

@@ -1,20 +1,26 @@
import os
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from utils.checks import is_owner_or_friend
class Invite(commands.Cog, name="invite"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(self, context: Context, embed: discord.Embed, *, ephemeral: bool = False) -> None:
async def send_embed(
self, context: Context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(embed=embed, ephemeral=ephemeral)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
@@ -22,7 +28,9 @@ class Invite(commands.Cog, name="invite"):
name="invite",
description="Get the invite link of the bot to be able to invite it.",
)
@commands.is_owner()
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@is_owner_or_friend()
async def invite(self, context: Context) -> None:
"""
Get the invite link of the bot to be able to invite it.
@@ -33,28 +41,28 @@ class Invite(commands.Cog, name="invite"):
if client is None:
await context.send("Bot is not ready. Try again shortly.")
return
permissions = os.getenv("INVITE_PERMISSIONS", "0")
invite_url = (
f"https://discord.com/api/oauth2/authorize?client_id={client.id}"
f"&scope=bot%20applications.commands&permissions={permissions}"
invite_link = os.getenv("INVITE_LINK")
embed = discord.Embed(
title="Install",
description=f"Install me by clicking [here]({invite_link}).",
color=0x7289DA,
)
embed = discord.Embed(title="Invite", description=f"Invite me by clicking [here]({invite_url}).", color=0x7289DA)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
try:
await context.author.send(embed=embed)
await self.send_embed(context, discord.Embed(description="I sent you a private message!", color=0x7289DA), ephemeral=True)
except discord.Forbidden:
await self.send_embed(context, embed, ephemeral=True)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=False)
async def cog_command_error(self, context: Context, error) -> None:
if isinstance(error, commands.NotOwner):
if isinstance(error, (commands.NotOwner, commands.CheckFailure)):
embed = discord.Embed(
title="Permission Denied",
description="You are not the owner of this bot.",
color=0xE02B2B
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed, ephemeral=True)
else:
raise error

150
cogs/owner/logs.py Normal file
View File

@@ -0,0 +1,150 @@
import os
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from utils.ascii_art import ascii_plain
from utils.checks import is_owner_or_friend
class Logs(commands.Cog, name="logs"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(
self, context: Context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
async def send_file(
self, context: Context, *, file: discord.File, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(file=file, ephemeral=ephemeral)
else:
await interaction.response.send_message(file=file, ephemeral=ephemeral)
else:
await context.send(file=file)
@commands.hybrid_command(
name="logs",
description="View the bot's log file",
)
@app_commands.describe(
lines="Number of lines to read from the end of the log file (default: 50, max: 200)"
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@is_owner_or_friend()
async def logs(self, context: Context, lines: int = 50) -> None:
if lines > 200:
lines = 200
elif lines < 1:
lines = 1
log_file_path = os.getenv("LOG_FILE", "logs/discord.log")
if not os.path.isabs(log_file_path):
log_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
log_file_path,
)
try:
if not os.path.exists(log_file_path):
embed = discord.Embed(
title="Error",
description=f"Log file not found at: `{log_file_path}`",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
return
with open(log_file_path, "r", encoding="utf-8", errors="replace") as f:
all_lines = f.readlines()
if not all_lines:
embed = discord.Embed(
title="Logs",
description="Log file is empty.",
color=0x7289DA,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
return
selected_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
log_content = "".join(selected_lines)
log_file = "logs.txt"
with open(log_file, "w", encoding="utf-8") as f:
f.write(
f"Bot logs extracted at {discord.utils.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
)
f.write(
f"Last {len(selected_lines)} lines from {os.path.basename(log_file_path)}\n"
)
f.write(f"Total lines in log file: {len(all_lines)}\n")
f.write("-" * 50 + "\n\n")
f.write(ascii_plain + "\n\n")
f.write(log_content)
file_obj = discord.File(log_file)
await self.send_file(context, file=file_obj, ephemeral=True)
os.remove(log_file)
except PermissionError:
embed = discord.Embed(
title="Error",
description=f"Permission denied when trying to read log file: `{log_file_path}`",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
except Exception as e:
embed = discord.Embed(
title="Error",
description=f"An error occurred while reading the log file:\n```\n{str(e)}\n```",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
async def cog_command_error(self, context: Context, error) -> None:
if isinstance(error, (commands.NotOwner, commands.CheckFailure)):
embed = discord.Embed(
title="Permission Denied",
description="You are not the owner of this bot!",
color=0xE02B2B,
)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
else:
raise error
async def setup(bot) -> None:
await bot.add_cog(Logs(bot))

View File

@@ -2,28 +2,42 @@ import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from utils.checks import is_owner_or_friend
class Say(commands.Cog, name="say"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(self, context: Context, embed: discord.Embed, *, ephemeral: bool = False) -> None:
async def send_embed(
self,
context: Context,
embed: discord.Embed,
*,
ephemeral: bool = False,
allowed_mentions: discord.AllowedMentions = None,
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
await interaction.followup.send(
embed=embed, ephemeral=ephemeral, allowed_mentions=allowed_mentions
)
else:
await interaction.response.send_message(embed=embed, ephemeral=ephemeral)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral, allowed_mentions=allowed_mentions
)
else:
await context.send(embed=embed)
await context.send(embed=embed, allowed_mentions=allowed_mentions)
@commands.hybrid_command(
name="say",
description="The bot will say anything you want.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(message="The message that should be repeated by the bot")
@commands.is_owner()
@is_owner_or_friend()
async def say(self, context: Context, *, message: str) -> None:
"""
The bot will say anything you want.
@@ -31,14 +45,47 @@ class Say(commands.Cog, name="say"):
:param context: The hybrid command context.
:param message: The message that should be repeated by the bot.
"""
await context.send(message)
if context.guild is not None:
self.bot.logger.info(
f"Say command used in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id}): {message}"
)
else:
self.bot.logger.info(
f"Say command used in DMs by {context.author} (ID: {context.author.id}): {message}"
)
allowed_mentions = discord.AllowedMentions.none()
interaction = getattr(context, "interaction", None)
if interaction is not None:
is_in_guild = context.guild is not None
await interaction.response.defer(ephemeral=is_in_guild)
try:
await context.channel.send(message, allowed_mentions=allowed_mentions)
if is_in_guild:
try:
await interaction.delete_original_response()
except:
pass
except discord.Forbidden:
await interaction.followup.send(
message, allowed_mentions=allowed_mentions
)
else:
try:
await context.message.delete()
except:
pass
await context.send(message, allowed_mentions=allowed_mentions)
@commands.hybrid_command(
name="embed",
description="The bot will say anything you want, but within embeds.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(message="The message that should be repeated by the bot")
@commands.is_owner()
@is_owner_or_friend()
async def embed(self, context: Context, *, message: str) -> None:
"""
The bot will say anything you want, but using embeds.
@@ -46,23 +93,59 @@ class Say(commands.Cog, name="say"):
:param context: The hybrid command context.
:param message: The message that should be repeated by the bot.
"""
if context.guild is not None:
self.bot.logger.info(
f"Embed command used in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id}): {message}"
)
else:
self.bot.logger.info(
f"Embed command used in DMs by {context.author} (ID: {context.author.id}): {message}"
)
allowed_mentions = discord.AllowedMentions.none()
embed = discord.Embed(
title="Say",
description=message,
color=0x7289DA,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed)
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
is_in_guild = context.guild is not None
await interaction.response.defer(ephemeral=is_in_guild)
try:
await context.channel.send(
embed=embed, allowed_mentions=allowed_mentions
)
if is_in_guild:
try:
await interaction.delete_original_response()
except:
pass
except discord.Forbidden:
await interaction.followup.send(
embed=embed, allowed_mentions=allowed_mentions
)
else:
try:
await context.message.delete()
except:
pass
await context.send(embed=embed, allowed_mentions=allowed_mentions)
async def cog_command_error(self, context: Context, error) -> None:
if isinstance(error, commands.NotOwner):
if isinstance(error, (commands.NotOwner, commands.CheckFailure)):
embed = discord.Embed(
title="Permission Denied",
description="You are not the owner of this bot!",
color=0xE02B2B,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
else:
raise error

View File

@@ -1,4 +1,7 @@
import os
import signal
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
@@ -6,14 +9,18 @@ from discord.ext.commands import Context
class Shutdown(commands.Cog, name="shutdown"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(self, context: Context, embed: discord.Embed, *, ephemeral: bool = False) -> None:
async def send_embed(
self, context: Context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(embed=embed, ephemeral=ephemeral)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
@@ -21,6 +28,8 @@ class Shutdown(commands.Cog, name="shutdown"):
name="shutdown",
description="Make the bot shutdown.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@commands.is_owner()
async def shutdown(self, context: Context) -> None:
"""
@@ -28,6 +37,16 @@ class Shutdown(commands.Cog, name="shutdown"):
:param context: The hybrid command context.
"""
# Log command execution before shutdown starts
if context.guild is not None:
self.bot.logger.info(
f"Executed shutdown command in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id})"
)
else:
self.bot.logger.info(
f"Executed shutdown command by {context.author} (ID: {context.author.id}) in DMs"
)
embed = discord.Embed(
title="Shutdown",
description="Shutting down. Bye! <a:PandaThanos:1417483671253811262>",
@@ -35,7 +54,7 @@ class Shutdown(commands.Cog, name="shutdown"):
).set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
await self.send_embed(context, embed)
await self.bot.close()
os.kill(os.getpid(), signal.SIGTERM)
async def cog_command_error(self, context: Context, error) -> None:
if isinstance(error, commands.NotOwner):
@@ -44,7 +63,9 @@ class Shutdown(commands.Cog, name="shutdown"):
description="You are not the owner of this bot!",
color=0xE02B2B,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
else:
raise error

View File

@@ -8,20 +8,26 @@ class Sync(commands.Cog, name="sync"):
def __init__(self, bot) -> None:
self.bot = bot
async def send_embed(self, context: Context, embed: discord.Embed, *, ephemeral: bool = False) -> None:
async def send_embed(
self, context: Context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(embed=embed, ephemeral=ephemeral)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
@commands.command(
@commands.hybrid_command(
name="sync",
description="Synchonizes the slash commands.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(scope="The scope of the sync. Can be `global` or `guild`")
@commands.is_owner()
async def sync(self, context: Context, scope: str) -> None:
@@ -38,7 +44,9 @@ class Sync(commands.Cog, name="sync"):
description="Slash commands have been globally synchronized.",
color=0x7289DA,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed)
return
elif scope == "guild":
@@ -49,7 +57,9 @@ class Sync(commands.Cog, name="sync"):
description="Slash commands have been synchronized in this guild.",
color=0x7289DA,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed)
return
embed = discord.Embed(
@@ -57,13 +67,17 @@ class Sync(commands.Cog, name="sync"):
description="The scope must be `global` or `guild`.",
color=0xE02B2B,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
@commands.command(
@commands.hybrid_command(
name="unsync",
description="Unsynchonizes the slash commands.",
)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.describe(
scope="The scope of the sync. Can be `global`, `current_guild` or `guild`"
)
@@ -83,7 +97,9 @@ class Sync(commands.Cog, name="sync"):
description="Slash commands have been globally unsynchronized.",
color=0x7289DA,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed)
return
elif scope == "guild":
@@ -94,7 +110,9 @@ class Sync(commands.Cog, name="sync"):
description="Slash commands have been unsynchronized in this guild.",
color=0x7289DA,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed)
return
embed = discord.Embed(
@@ -102,10 +120,11 @@ class Sync(commands.Cog, name="sync"):
description="The scope must be `global` or `guild`.",
color=0xE02B2B,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
async def cog_command_error(self, context: Context, error) -> None:
if isinstance(error, commands.NotOwner):
embed = discord.Embed(
@@ -113,7 +132,9 @@ class Sync(commands.Cog, name="sync"):
description="You are not the owner of this bot!",
color=0xE02B2B,
)
embed.set_author(name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp")
embed.set_author(
name="Owner", icon_url="https://yes.nighty.works/raw/zReOib.webp"
)
await self.send_embed(context, embed, ephemeral=True)
else:
raise error

197
cogs/sidestore/__init__.py Normal file
View File

@@ -0,0 +1,197 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .sidestore import SidestoreView
from .refresh import refresh_command
from .code import code_command
from .crash import crash_command
from .pairing import pairing_command
from .server import server_command
from .afc import afc_command
from .udid import udid_command
from .half import half_command
from .sparse import sparse_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Sidestore(commands.GroupCog, name="sidestore"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="sidestore", invoke_without_command=True)
async def sidestore_group(self, context: Context):
embed = discord.Embed(
title="SideStore Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x8E82F9,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
view = SidestoreView(self.bot)
await context.send(embed=embed, view=view)
@sidestore_group.command(name="help")
async def sidestore_group_help(self, context: Context):
embed = discord.Embed(
title="SideStore Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x8E82F9,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
view = SidestoreView(self.bot)
await context.send(embed=embed, view=view)
async def _invoke_hybrid(self, context: Context, name: str):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command)
else:
await context.send(f"Unknown SideStore command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@sidestore_group.command(name="refresh")
async def sidestore_group_refresh(self, context: Context):
await self._invoke_hybrid(context, "refresh")
@sidestore_group.command(name="code")
async def sidestore_group_code(self, context: Context):
await self._invoke_hybrid(context, "code")
@sidestore_group.command(name="crash")
async def sidestore_group_crash(self, context: Context):
await self._invoke_hybrid(context, "crash")
@sidestore_group.command(name="pairing")
async def sidestore_group_pairing(self, context: Context):
await self._invoke_hybrid(context, "pairing")
@sidestore_group.command(name="server")
async def sidestore_group_server(self, context: Context):
await self._invoke_hybrid(context, "server")
@sidestore_group.command(name="afc")
async def sidestore_group_afc(self, context: Context):
await self._invoke_hybrid(context, "afc")
@sidestore_group.command(name="udid")
async def sidestore_group_udid(self, context: Context):
await self._invoke_hybrid(context, "udid")
@sidestore_group.command(name="half")
async def sidestore_group_half(self, context: Context):
await self._invoke_hybrid(context, "half")
@sidestore_group.command(name="sparse")
async def sidestore_group_sparse(self, context: Context):
await self._invoke_hybrid(context, "sparse")
@app_commands.command(name="help", description="SideStore troubleshooting help")
async def help(self, interaction: discord.Interaction):
embed = discord.Embed(
title="SideStore Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x8E82F9,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
view = SidestoreView(self.bot)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="refresh", description="Help with refreshing or installing apps"
)
async def refresh(self, context):
return await refresh_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="code", description="No code received when signing in with Apple ID"
)
async def code(self, context):
return await code_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="crash", description="Help with SideStore crashing issues"
)
async def crash(self, context):
return await crash_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="pairing", description="Help with pairing file issues"
)
async def pairing(self, context):
return await pairing_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="server", description="Help with anisette server issues"
)
async def server(self, context):
return await server_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="afc", description="Help with AFC Connection Failure issues"
)
async def afc(self, context):
return await afc_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="udid", description="SideStore could not determine device UDID"
)
async def udid(self, context):
return await udid_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(name="half", description="Help with half-installed apps")
async def half(self, context):
return await half_command()(self, context)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="sparse", description="Help with sparse bundle issues"
)
async def sparse(self, context):
return await sparse_command()(self, context)
async def setup(bot) -> None:
cog = Sidestore(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'sidestore.help'")
bot.logger.info("Loaded extension 'sidestore.refresh'")
bot.logger.info("Loaded extension 'sidestore.code'")
bot.logger.info("Loaded extension 'sidestore.crash'")
bot.logger.info("Loaded extension 'sidestore.pairing'")
bot.logger.info("Loaded extension 'sidestore.server'")
bot.logger.info("Loaded extension 'sidestore.afc'")
bot.logger.info("Loaded extension 'sidestore.udid'")
bot.logger.info("Loaded extension 'sidestore.half'")
bot.logger.info("Loaded extension 'sidestore.sparse'")

49
cogs/sidestore/afc.py Normal file
View File

@@ -0,0 +1,49 @@
import discord
from discord.ext import commands
def afc_command():
@commands.hybrid_command(
name="afc", description="Help with AFC Connection Failure issues"
)
async def afc(self, context):
embed = discord.Embed(
color=0x8E82F9,
description=(
"# AFC Connection Failure\n\n---\n\n"
+ "1. Make sure Wi-Fi is connected to a stable network\n"
+ "2. Make sure LocalDevVPN is connected and updated\n"
+ "3. If issue still persists, replace pairing file using `iloader`. See [Pairing File instructions](https://docs.sidestore.io/docs/advanced/pairing-file) for details"
),
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/afc.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/common-issues/#afc-connection-failure--no-wi-fi-or-vpn-connection",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return afc

View File

@@ -1,63 +1,60 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Code(commands.Cog, name="code"):
def __init__(self, bot) -> None:
self.bot = bot
def code_command():
@commands.hybrid_command(
name="code", description="No code received when signing in with Apple ID"
)
async def code(self, context: Context) -> None:
async def code(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'## Verification Code Not Received When Signing In with Apple ID\n\n---\n\n' +
'1. **For iOS versions below 18.1:**\n' +
' - Open the "Settings" app\n' +
' - Tap on your name at the top of the screen\n' +
' - Navigate to "Sign-In and Security"\n' +
' - Select "Two-Factor Authentication"\n' +
' - Choose "Get Verification Code"\n\n' +
'2. **For iOS versions 18.1 and above:**\n' +
' - Visit [iCloud](https://www.icloud.com) on a web browser\n' +
' - Click "Sign In"\n' +
' - When prompted, you will see two options: "Sign In" and "Use Different Apple Account"\n' +
' - Select the bottom option, "Use Different Apple Account"\n' +
' - Enter your Apple ID and password\n' +
' - Apple will send you a verification code\n' +
' - Use this code in Sidestore to complete the sign-in process\n'
"## Verification Code Not Received When Signing In with Apple ID\n\n---\n\n"
+ "1. **For iOS versions below 18.1:**\n"
+ ' - Open the "Settings" app\n'
+ " - Tap on your name at the top of the screen\n"
+ ' - Navigate to "Sign-In and Security"\n'
+ ' - Select "Two-Factor Authentication"\n'
+ ' - Choose "Get Verification Code"\n\n'
+ "2. **For iOS versions 18.1 and above:**\n"
+ " - Visit [iCloud](https://www.icloud.com) on a web browser\n"
+ ' - Click "Sign In"\n'
+ ' - On an Apple device, you may see two options: "Sign In" and "Use Different Apple Account"\n'
+ ' - Select the bottom option, "Use Different Apple Account"\n'
+ " - Enter your Apple ID and password, DO NOT USE A PASSKEY\n"
+ " - Apple will send you a verification code\n"
+ " - Use this code in SideStore to complete the sign-in process\n"
),
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/code.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/#sign-in-issues",
emoji="<:sidestorepride:1417717648795631787>",
)
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/code.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/#sign-in-issues",
emoji="<:sidestorepride:1417717648795631787>"
))
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Code(bot))
return code

View File

@@ -1,47 +1,46 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Crash(commands.Cog, name="crash"):
def __init__(self, bot) -> None:
self.bot = bot
def crash_command():
@commands.hybrid_command(
name="crash", description="Help with SideStore crashing issues"
)
async def crash(self, context: Context) -> None:
async def crash(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'# Sidestore Crashing After Refresh\n\n---\n\n' +
'1. Delete your current SideStore.\n' +
'2. Reinstall with AltServer.\n' +
'3. Select the pairing file and sign into SideStore.\n' +
'4. Download the SideStore .ipa file, and save it to your Files app.\n' +
'5. Import the "Sidestore.ipa" file into SideStore, just like how you import any other IPA.\n\n' +
'This process ensures SideStore is refreshed without issues.'
)
"# SideStore Crashing After Refresh\n\n---\n\n"
+ "First, to try and save your data:\n"
+ "1. DON'T DELETE SIDESTORE, reinstall using iloader's Install `SideStore (Stable)` button.\n"
+ "If that doesn't work:\n"
+ "1. Delete your current SideStore. Reinstall with iloader.\n"
+ "2. Import your pairing file and sign into SideStore.\n"
+ "3. Download the SideStore .ipa file, and save it to your Files app.\n"
+ '4. Import the "SideStore.ipa" file into SideStore, just like how you import any other IPA.\n\n'
+ "This process ensures SideStore is refreshed without issues."
),
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/crash.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/crash.py",
emoji="<:githubicon:1417717356846776340>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Crash(bot))
return crash

View File

@@ -1,62 +1,55 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Half(commands.Cog, name="half"):
def __init__(self, bot) -> None:
self.bot = bot
def half_command():
@commands.hybrid_command(
name="half", description="Help when apps get stuck installing"
)
async def half(self, context: Context) -> None:
async def half(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'# Sidestore/IPAs Stuck Halfway Through Installing or Refreshing\n\n---\n' +
'### Method 1: Basic Troubleshooting\n\n' +
'- Restart SideStore\n' +
'- Restart device\n' +
'- Clear Cache\n' +
'- Change Anisette Server\n' +
'- Reset adi.pb\n' +
'- Sign out from SideStore and sign back in\n' +
'- Regenerate pairing file\n' +
'- Reinstall SideStore\n\n' +
'### Method 2: If Method 1 Doesn\'t Work\n\n' +
'1. Delete Sidestore\n' +
'2. Reinstall SideStore using the guide at https://docs.sidestore.io/\n' +
'3. **Do not use the IPA provided by the website**, instead use this older version: [Sidestore 0.5.9 download](https://github.com/SideStore/SideStore/releases/download/0.5.9/SideStore.ipa)\n' +
'4. Setup SideStore and StosVPN as usual\n' +
'> -# Step 3 is the important one (make sure to do that)\n'
)
"# SideStore/IPAs Stuck Halfway Through Installing or Refreshing\n\n---\n"
+ "- Make sure you are on the latest version of SideStore\n"
+ "- Restart SideStore\n"
+ "- Restart device\n"
+ "- Clear Cache\n"
+ "- Change Anisette Server\n"
+ "- Reset adi.pb\n"
+ "- Sign out from SideStore and sign back in\n"
+ "- Replace pairing file\n"
+ "- Reinstall SideStore\n\n"
),
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/half.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/common-issues#sidestore-hangs-halfway-through-installation",
emoji="<:sidestorepride:1417717648795631787>"
))
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/half.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/common-issues/#sidestore-hangs-partway-through-installation",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Half(bot))
return half

View File

@@ -1,61 +1,45 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Pairing(commands.Cog, name="pairing"):
def __init__(self, bot) -> None:
self.bot = bot
@commands.hybrid_command(
name="pairing", description="Help with pairing file issues"
)
async def pairing(self, context: Context) -> None:
def pairing_command():
@commands.hybrid_command(name="pairing", description="Link to the pairing guide")
async def pairing(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'# Cannot Choose Pairing File\n\n---\n\n' +
'1. **Check File Extension:**\n' +
" Make sure your pairing file's extension ends with `.mobiledevicepairing` or `.plist`\n" +
' - If it doesn\'t, double-check to see if you had zipped your pairing file before sending it to your phone. Failing to do so may lead to the file being corrupted during transport\n\n' +
'2. **Move Pairing File:**\n' +
' If you are unable to select the pairing file from within the app:\n' +
' - Rename the file to `ALTPairingFile.mobiledevicepairing`\n' +
' - Try moving the pairing file to the root directory of the SideStore folder in the Files app under "On My iPhone/iPad"\n\n' +
'3. **Certificate Signing:**\n' +
" When signing Sidestore with certain certificates, you won't be able to select the pairing file from within the app\n" +
' - Try the fix mentioned above\n' +
' - If you do not see the Sidestore folder in the Files app:\n' +
' • Connect your phone to your computer\n' +
' • Drag and drop the pairing file into the Sidestore app\'s files section\n' +
' • Ensure the file is renamed to `ALTPairingFile.mobiledevicepairing`\n'
)
"# How to obtain your pairing file:\n\n---\n\n"
+ "[Click here](https://docs.sidestore.io/advanced/pairing-file) to read the SideStore documentation on replacing your pairing file.\n"
),
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/pairing.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/#cannot-choose-pairing-file",
emoji="<:sidestorepride:1417717648795631787>"
))
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/pairing.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/advanced/pairing-file",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Pairing(bot))
return pairing

View File

@@ -1,53 +1,50 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Refresh(commands.Cog, name="refresh"):
def __init__(self, bot) -> None:
self.bot = bot
def refresh_command():
@commands.hybrid_command(
name="refresh", description="Help with refreshing or installing apps"
)
async def refresh(self, context: Context) -> None:
async def refresh(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'# Can\'t Refresh or Install Apps\n\n---\n\n' +
'1. Make sure your device is connected to a stable Wi-Fi network and not using cellular data.\n' +
'2. Verify VPN is connected in the StosVPN app.\n' +
'3. Turn off, then turn back on StosVPN, and wait a few seconds in Sidestore before trying to refresh.\n' +
'4. **Create a brand new pairing file.**\n' +
' - If none of the above worked, it is very likely that the pairing file is corrupted. You can reference the documentation on how to create a new pairing file [here](https://docs.sidestore.io/docs/troubleshooting/#cant-refresh-or-install-apps).\n' +
' - After creating a new pairing file, go to Sidestore settings and press "Reset pairing file," then choose the new pairing file you just created.\n'
)
"# Can't Refresh or Install Apps\n\n---\n\n"
+ "1. Make sure your device is connected to a stable Wi-Fi network and not using cellular data.\n"
+ "2. Verify VPN is connected in the LocalDevVPN app.\n"
+ "3. **Replace your pairing file.**\n"
+ " - If none of the above worked, it is very likely that the pairing file is corrupted. You can reference the documentation on how to replace your pairing file [here](https://docs.sidestore.io/docs/advanced/pairing-file).\n"
),
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/refresh.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/#cant-refresh-or-install-apps",
emoji="<:sidestorepride:1417717648795631787>"
))
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/refresh.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/advanced/pairing-file",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Refresh(bot))
return refresh

View File

@@ -1,55 +1,54 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Server(commands.Cog, name="server"):
def __init__(self, bot) -> None:
self.bot = bot
def server_command():
@commands.hybrid_command(
name="server", description="Help with anisette server issues"
)
async def server(self, context: Context) -> None:
async def server(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'# Sidestore Freezing or Displaying an Error Code During Sign-In\n\n---\n\n' +
'1. **Change the Anisette Server:**\n' +
' The most common solution is to switch to a different Anisette server. Do this:\n' +
' - Open Sidestore settings\n' +
' - Scroll down to the "Anisette Server" option\n' +
' - Select a different server from the list\n' +
' - You might need to try a few servers from the list and find which works best for you\n\n' +
'2. **Host Your Own Anisette Server:**\n' +
' If you prefer, you can set up your own Anisette server. Detailed instructions for hosting an Anisette server are available in the official documentation and can be found [here](https://docs.sidestore.io/docs/advanced/anisette).\n\n'
)
"# SideStore Freezing or Displaying an Error Code During Sign-In\n\n---\n\n"
+ "1. **Change the Anisette Server:**\n"
+ " The most common solution is to switch to a different Anisette server. Do this:\n"
+ " - Open Sidestore settings\n"
+ ' - Scroll down to the "Anisette Server" option\n'
+ " - Select a different server from the list\n"
+ " - You might need to try a few servers from the list and find which works best for you\n\n"
+ "2. **Host Your Own Anisette Server:**\n"
+ " If you prefer, you can set up your own Anisette server. Detailed instructions for hosting an Anisette server are available in the official documentation and can be found [here](https://docs.sidestore.io/docs/advanced/anisette).\n\n"
),
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/server.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/#sidestore-freezing-or-displaying-an-error-code-during-sign-in",
emoji="<:sidestorepride:1417717648795631787>"
))
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/server.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/troubleshooting/#sidestore-freezing-or-displaying-an-error-code-during-sign-in",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Server(bot))
return server

View File

@@ -1,8 +1,5 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class SidestoreSelect(discord.ui.Select):
@@ -43,32 +40,86 @@ class SidestoreSelect(discord.ui.Select):
label="SparseRestore",
value="sparse",
description="Information about SparseRestore exploit",
)
),
discord.SelectOption(
label="AFC Connection Failure",
value="afc",
description="Help with AFC Connection Failure issues",
),
discord.SelectOption(
label="UDID Error",
value="udid",
description="SideStore could not determine device UDID",
),
]
super().__init__(placeholder="Choose a SideStore command...", options=options)
async def callback(self, interaction: discord.Interaction):
command_name = self.values[0]
command = self.bot.get_command(command_name)
if command:
ctx = await self.bot.get_context(interaction.message)
if ctx:
await ctx.invoke(command)
embed = discord.Embed(
title="Command Executed",
description=f"Successfully executed `/{command_name}`",
color=0x00FF00
try:
ctx = await self.bot.get_context(interaction.message)
if ctx:
await ctx.invoke(command)
embed = discord.Embed(
title="Command Executed",
description=f"Successfully executed `/{command_name}`",
color=0x00FF00,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
await interaction.response.edit_message(embed=embed, view=None)
except discord.Forbidden:
guild_info = (
f"server {interaction.guild.name} (ID: {interaction.guild.id})"
if interaction.guild
else "DM or private channel"
)
self.bot.logger.warning(
f"Bot missing permissions in {guild_info} - cannot execute {command_name} command"
)
if interaction.guild is None:
embed = discord.Embed(
title="Error",
description="This command cannot be executed in DMs.",
color=0xFF0000,
)
else:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xFF0000,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
await interaction.response.edit_message(embed=embed, view=None)
except Exception as e:
self.bot.logger.error(f"Error executing {command_name} command: {e}")
embed = discord.Embed(
title="Error",
description="An error occurred while executing the command.",
color=0xFF0000,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
await interaction.response.edit_message(embed=embed, view=None)
else:
embed = discord.Embed(
title="Error",
description="Command not found!",
color=0xFF0000
title="Error", description="Command not found!", color=0xFF0000
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
await interaction.response.edit_message(embed=embed, view=None)
@@ -78,28 +129,66 @@ class SidestoreView(discord.ui.View):
self.add_item(SidestoreSelect(bot))
class Sidestore(commands.Cog, name="sidestore"):
def __init__(self, bot) -> None:
self.bot = bot
def sidestore_command():
@commands.hybrid_command(
name="sidestore", description="SideStore troubleshooting and help"
name="help", description="SideStore troubleshooting and help"
)
async def sidestore(self, context: Context) -> None:
async def sidestore(self, context):
if isinstance(context.channel, discord.DMChannel):
embed = discord.Embed(
title="Error",
description="This command can only be used in servers.",
color=0xE02B2B,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
await context.send(embed=embed, ephemeral=True)
return
if isinstance(context.channel, discord.PartialMessageable):
embed = discord.Embed(
title="Error",
description="The bot needs send messages permissions in this channel.",
color=0xE02B2B,
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
if context.interaction:
await context.interaction.response.send_message(
embed=embed, ephemeral=True
)
else:
await context.send(embed=embed, ephemeral=True)
return
embed = discord.Embed(
title="SideStore Commands",
description="Choose a command from the dropdown below to get help with specific issues:",
color=0x8e82f9
color=0x8E82F9,
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
view = SidestoreView(self.bot)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view, ephemeral=True)
await context.interaction.response.send_message(
embed=embed, view=view, ephemeral=True
)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Sidestore(bot))
return sidestore

View File

@@ -1,50 +1,49 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
import time
class Sparse(commands.Cog, name="sparse"):
def __init__(self, bot) -> None:
self.bot = bot
def sparse_command():
@commands.hybrid_command(
name="sparse", description="Information about SparseRestore exploit"
)
async def sparse(self, context: Context) -> None:
async def sparse(self, context):
embed = discord.Embed(
color=0x8e82f9,
color=0x8E82F9,
description=(
'# SparseRestore "Bypass 3 App Limit" Exploit\n\n---\n\n' +
'The SparseRestore exploit allows you to bypass the 3-app sideloading limit. It is compatible with iOS/iPadOS versions **15.2 to 18.1 beta 4**, (not including **17.7.1** and **17.7.2**).\n\n' +
'iOS/iPadOS versions **17.0** (not including **16.7** and **16.7.10**) are recommended to use [Trollstore](https://ios.cfw.guide/installing-trollstore/)\n\n' +
'If you\'re on a supported version and want to sideload more than three apps, follow the detailed instructions found in our documentation'
)
'# SparseRestore "Bypass 3 App Limit" Exploit\n\n---\n\n'
+ "The SparseRestore exploit allows you to bypass the 3-app sideloading limit. It is compatible with iOS/iPadOS versions **15.2 to 18.1 beta 4**, (not including **17.7.1** and **17.7.2**).\n\n"
+ "iOS/iPadOS versions **17.0** (not including **16.7** and **16.7.10**) are recommended to use [Trollstore](https://ios.cfw.guide/installing-trollstore/)\n\n"
+ "If you're on a supported version and want to sideload more than three apps, follow the detailed instructions found in our documentation"
),
)
embed.set_author(name="SideStore", icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true")
embed.set_footer(text=f'Last Edited by neoarz')
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by neoarz")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/sparse.py",
emoji="<:githubicon:1417717356846776340>"
))
view.add_item(discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/advanced/sparserestore",
emoji="<:sidestorepride:1417717648795631787>"
))
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/sparse.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.primary,
url="https://docs.sidestore.io/docs/advanced/alternative#sparserestore-3-app-limit",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
async def setup(bot) -> None:
await bot.add_cog(Sparse(bot))
return sparse

48
cogs/sidestore/udid.py Normal file
View File

@@ -0,0 +1,48 @@
import discord
from discord.ext import commands
def udid_command():
@commands.hybrid_command(
name="udid", description="SideStore could not determine device UDID"
)
async def udid(self, context):
embed = discord.Embed(
color=0x8E82F9,
description=(
"# SideStore Could Not Determine Device UDID\n\n---\n\n"
+ "This error usually occurs when the pairing file is corrupted. Please replace your pairing file using `iloader` and try again.\n\n"
+ "If you forgot how to replace your pairing file, you can refer to the documentation below."
),
)
embed.set_author(
name="SideStore",
icon_url="https://github.com/SideStore/assets/blob/main/icons/classic/Default.png?raw=true",
)
embed.set_footer(text="Last Edited by CelloSerenity")
embed.timestamp = discord.utils.utcnow()
view = discord.ui.View()
view.add_item(
discord.ui.Button(
label="Edit Command",
style=discord.ButtonStyle.secondary,
url="https://github.com/neoarz/Syntrel/blob/main/cogs/sidestore/udid.py",
emoji="<:githubicon:1417717356846776340>",
)
)
view.add_item(
discord.ui.Button(
label="Documentation",
style=discord.ButtonStyle.secondary,
url="https://docs.sidestore.io/docs/advanced/pairing-file/",
emoji="<:sidestorepride:1417717648795631787>",
)
)
if context.interaction:
await context.interaction.response.send_message(embed=embed, view=view)
else:
await context.send(embed=embed, view=view)
return udid

118
cogs/utilities/__init__.py Normal file
View File

@@ -0,0 +1,118 @@
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from .translate import translate_command, language_autocomplete
from .codepreview import codepreview_command
from .dictionary import dictionary_command
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.allowed_installs(guilds=True, users=True)
class Utilities(commands.GroupCog, name="utils"):
def __init__(self, bot) -> None:
self.bot = bot
super().__init__()
@commands.group(name="utilities", aliases=["utils"], invoke_without_command=True)
async def utilities_group(self, context: Context):
embed = discord.Embed(
title="Utilities Commands",
description="Use `.utils <subcommand>` or `/utils <subcommand>`.",
color=0x7289DA,
)
embed.set_author(
name="Utilities", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
embed.add_field(
name="Available", value="translate, codepreview, dictionary", inline=False
)
await context.send(embed=embed)
async def _invoke_hybrid(self, context: Context, name: str, **kwargs):
command = self.bot.get_command(name)
if command is not None:
await context.invoke(command, **kwargs)
else:
await context.send(f"Unknown utilities command: {name}")
def _require_group_prefix(context: Context) -> bool:
if getattr(context, "interaction", None):
return True
group = getattr(getattr(context, "cog", None), "qualified_name", "").lower()
if not group:
return True
prefix = context.prefix or ""
content = context.message.content.strip().lower()
return content.startswith(f"{prefix}{group} ")
@utilities_group.command(name="translate")
@app_commands.describe(
text="The text to translate",
to_lang="Target language (e.g., 'en', 'es', 'fr')",
from_lang="Source language (leave empty for auto-detect)",
)
@app_commands.autocomplete(to_lang=language_autocomplete)
@app_commands.autocomplete(from_lang=language_autocomplete)
async def utilities_group_translate(
self,
context: Context,
text: str = None,
to_lang: str = "en",
from_lang: str = None,
):
await self._invoke_hybrid(
context, "translate", text=text, to_lang=to_lang, from_lang=from_lang
)
@utilities_group.command(name="codepreview")
async def utilities_group_codepreview(self, context: Context, url: str = None):
await self._invoke_hybrid(context, "codepreview", url=url)
@utilities_group.command(name="dictionary")
@app_commands.describe(word="The word to look up")
async def utilities_group_dictionary(self, context: Context, word: str = None):
await self._invoke_hybrid(context, "dictionary", word=word)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="translate", description="Translate text to another language"
)
@app_commands.describe(
text="The text to translate",
to_lang="Target language (e.g., 'en', 'es', 'fr')",
from_lang="Source language (leave empty for auto-detect)",
)
@app_commands.autocomplete(to_lang=language_autocomplete)
@app_commands.autocomplete(from_lang=language_autocomplete)
async def translate(
self, context, text: str = None, to_lang: str = "en", from_lang: str = None
):
return await translate_command()(
self, context, text=text, to_lang=to_lang, from_lang=from_lang
)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="codepreview", description="Preview code from GitHub URLs"
)
async def codepreview(self, context, url: str = None):
return await codepreview_command()(self, context, url=url)
@commands.check(_require_group_prefix)
@commands.hybrid_command(
name="dictionary", description="Get the definition of a word"
)
@app_commands.describe(word="The word to look up")
async def dictionary(self, context, word: str = None):
return await dictionary_command()(self, context, word=word)
async def setup(bot) -> None:
cog = Utilities(bot)
await bot.add_cog(cog)
bot.logger.info("Loaded extension 'utilities.translate'")
bot.logger.info("Loaded extension 'utilities.codepreview'")
bot.logger.info("Loaded extension 'utilities.dictionary'")

View File

@@ -0,0 +1,526 @@
import discord
from discord import app_commands
from discord.ext import commands
import aiohttp
import re
def codepreview_command():
LANGUAGE_MAP = {
".py": "python",
".js": "js",
".ts": "ts",
".java": "java",
".cpp": "cpp",
".c": "c",
".cs": "cs",
".go": "go",
".rs": "rust",
".rb": "ruby",
".php": "php",
".html": "html",
".css": "css",
".json": "json",
".xml": "xml",
".yaml": "yaml",
".yml": "yaml",
".ini": "ini",
".toml": "toml",
".lua": "lua",
".sh": "bash",
".md": "markdown",
".sql": "sql",
".diff": "diff",
".txt": "",
}
async def send_embed(
context, embed: discord.Embed, *, ephemeral: bool = False
) -> None:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
else:
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
else:
await context.send(embed=embed)
def get_language_from_filename(filename):
for ext, lang in LANGUAGE_MAP.items():
if filename.endswith(ext):
return lang
return ""
async def fetch_github_content(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
except Exception:
pass
return None
async def fetch_pr_diff(owner, repo, pr_number):
try:
api_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
headers = {
"Accept": "application/vnd.github.v3.diff",
"User-Agent": "Discord-Bot",
}
async with aiohttp.ClientSession() as session:
async with session.get(api_url, headers=headers) as response:
if response.status == 200:
diff_content = await response.text()
return diff_content
except Exception:
pass
return None
async def fetch_pr_info(owner, repo, pr_number):
try:
api_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
headers = {
"Accept": "application/vnd.github.v3+json",
"User-Agent": "Discord-Bot",
}
async with aiohttp.ClientSession() as session:
async with session.get(api_url, headers=headers) as response:
if response.status == 200:
pr_data = await response.json()
return {
"title": pr_data.get("title", ""),
"number": pr_data.get("number", pr_number),
"state": pr_data.get("state", ""),
"merged": pr_data.get("merged", False),
"additions": pr_data.get("additions", 0),
"deletions": pr_data.get("deletions", 0),
"changed_files": pr_data.get("changed_files", 0),
"user": pr_data.get("user", {}).get("login", ""),
"base_branch": pr_data.get("base", {}).get("ref", ""),
"head_branch": pr_data.get("head", {}).get("ref", ""),
}
except Exception:
pass
return None
def parse_github_url(url):
pr_pattern = r"https://github\.com/([^/]+)/([^/]+)/pull/(\d+)(?:/files)?"
pr_match = re.match(pr_pattern, url)
if pr_match:
owner, repo, pr_number = pr_match.groups()
return {"type": "pr", "owner": owner, "repo": repo, "pr_number": pr_number}
raw_pattern = (
r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+?)$"
)
raw_match = re.match(raw_pattern, url)
if raw_match:
owner, repo, branch, filepath = raw_match.groups()
return {
"type": "file",
"owner": owner,
"repo": repo,
"branch": branch,
"filepath": filepath,
"raw_url": url,
"start_line": None,
"end_line": None,
}
pattern = r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+?)(?:#L(\d+)(?:-L(\d+))?)?$"
match = re.match(pattern, url)
if match:
owner, repo, branch, filepath, start_line, end_line = match.groups()
raw_url = (
f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{filepath}"
)
return {
"type": "file",
"owner": owner,
"repo": repo,
"branch": branch,
"filepath": filepath,
"raw_url": raw_url,
"start_line": int(start_line) if start_line else None,
"end_line": int(end_line) if end_line else None,
}
return None
def extract_lines(content, start_line, end_line):
lines = content.split("\n")
if start_line and end_line:
return "\n".join(lines[start_line - 1 : end_line])
elif start_line:
return lines[start_line - 1] if start_line <= len(lines) else content
return content
@commands.hybrid_command(
name="codepreview",
description="Preview code from GitHub URLs",
)
@app_commands.describe(url="GitHub URL to preview code from")
async def codepreview(self, context, url: str = None):
if isinstance(context.channel, discord.DMChannel):
embed = discord.Embed(
title="Error",
description="This command can only be used in servers.",
color=0xE02B2B,
)
embed.set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if isinstance(context.channel, discord.PartialMessageable):
embed = discord.Embed(
title="Error",
description="The bot needs the `send messages` permission in this channel.",
color=0xE02B2B,
)
embed.set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
if not url or not url.strip():
if (
context.message
and context.message.reference
and context.message.reference.resolved
):
replied_message = context.message.reference.resolved
if hasattr(replied_message, "content") and replied_message.content:
github_pattern = (
r"https://(?:github\.com|raw\.githubusercontent\.com)/[^\s]+"
)
urls = re.findall(github_pattern, replied_message.content)
if urls:
url = urls[0]
else:
embed = discord.Embed(
title="Error",
description="No GitHub URL found in the replied message.",
color=0xE02B2B,
).set_author(
name="Utility",
icon_url="https://yes.nighty.works/raw/8VLDcg.webp",
)
await send_embed(context, embed, ephemeral=True)
return
else:
embed = discord.Embed(
title="Error",
description="The replied message has no content to extract GitHub URL from.",
color=0xE02B2B,
).set_author(
name="Utility",
icon_url="https://yes.nighty.works/raw/8VLDcg.webp",
)
await send_embed(context, embed, ephemeral=True)
return
else:
embed = discord.Embed(
title="Error",
description="Please provide a GitHub URL or reply to a message containing a GitHub URL.",
color=0xE02B2B,
).set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
await send_embed(context, embed, ephemeral=True)
return
if not url.startswith("https://github.com/") and not url.startswith(
"https://raw.githubusercontent.com/"
):
embed = discord.Embed(
title="Error",
description="Please provide a valid GitHub URL.",
color=0xE02B2B,
).set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
await send_embed(context, embed, ephemeral=True)
return
# Check if bot has send messages permission before starting processing
try:
test_embed = discord.Embed(title="Testing permissions...", color=0x7289DA)
test_embed.set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
await context.channel.send(embed=test_embed, delete_after=0.1)
except discord.Forbidden:
embed = discord.Embed(
title="Permission Error",
description="The bot needs the `send messages` permission to execute this command.",
color=0xE02B2B,
)
embed.set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.send_message(embed=embed, ephemeral=True)
else:
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await context.send(embed=embed, ephemeral=True)
return
parsed = parse_github_url(url)
if not parsed:
embed = discord.Embed(
title="Error",
description="Invalid GitHub URL format. Please provide a valid GitHub blob URL or PR URL.",
color=0xE02B2B,
).set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
await send_embed(context, embed, ephemeral=True)
return
if parsed.get("type") == "pr":
pr_info = await fetch_pr_info(
parsed["owner"], parsed["repo"], parsed["pr_number"]
)
diff_content = await fetch_pr_diff(
parsed["owner"], parsed["repo"], parsed["pr_number"]
)
if not pr_info or not diff_content:
embed = discord.Embed(
title="Error",
description="Failed to fetch pull request information. The PR might not exist or be accessible.",
color=0xE02B2B,
).set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
await send_embed(context, embed, ephemeral=True)
return
pr_url = f"https://github.com/{parsed['owner']}/{parsed['repo']}/pull/{parsed['pr_number']}"
if pr_info["merged"]:
pr_color = 0x6F42C1
pr_status = "Merged"
elif pr_info["state"] == "open":
pr_color = 0x57F287
pr_status = "Open"
else:
pr_color = 0xE02B2B
pr_status = "Closed"
embed = discord.Embed(
title=f"Pull Request #{pr_info['number']}: {pr_info['title'][:100]}",
description=f"**Repository:** [{parsed['owner']}/{parsed['repo']}]({pr_url})\n**Author:** {pr_info['user']}\n**Status:** {pr_status}",
color=pr_color,
)
embed.set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
embed.add_field(
name="Changes",
value=f"**+{pr_info['additions']}** / **-{pr_info['deletions']}**",
inline=True,
)
embed.add_field(
name="Files Changed", value=f"{pr_info['changed_files']}", inline=True
)
embed.add_field(
name="Branches",
value=f"`{pr_info['base_branch']}` ← `{pr_info['head_branch']}`",
inline=False,
)
embed.set_footer(
text=f"Requested by {context.author.name}",
icon_url=context.author.display_avatar.url,
)
interaction = getattr(context, "interaction", None)
if interaction is not None and not interaction.response.is_done():
await interaction.response.defer(ephemeral=True)
await context.channel.send(embed=embed)
max_diff_length = 1900
max_lines = 100
diff_lines = diff_content.split("\n")
if len(diff_lines) > max_lines:
diff_lines = diff_lines[:max_lines]
diff_lines.append(
f"\n... ({len(diff_content.split(chr(10))) - max_lines} more lines omitted)"
)
current_chunk = ""
for line in diff_lines:
test_chunk = current_chunk + line + "\n"
if len(test_chunk) + 10 > max_diff_length:
if current_chunk.strip():
remaining_lines = len(diff_lines) - len(
current_chunk.split("\n")
)
if remaining_lines > 0:
current_chunk += (
f"\n... ({remaining_lines} more lines omitted)"
)
await context.channel.send(
f"```diff\n{current_chunk.rstrip()}\n```"
)
break
else:
current_chunk = test_chunk
else:
if current_chunk.strip():
await context.channel.send(
f"```diff\n{current_chunk.rstrip()}\n```"
)
if interaction is not None:
try:
await interaction.delete_original_response()
except:
pass
return
content = await fetch_github_content(parsed["raw_url"])
if not content:
embed = discord.Embed(
title="Error",
description="Failed to fetch content from GitHub. The file might not exist or be accessible.",
color=0xE02B2B,
).set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
await send_embed(context, embed, ephemeral=True)
return
if parsed["start_line"]:
code = extract_lines(content, parsed["start_line"], parsed["end_line"])
line_info = f" (Lines {parsed['start_line']}"
if parsed["end_line"]:
line_info += f"-{parsed['end_line']}"
line_info += ")"
else:
code = content
line_info = ""
code_lines = code.split("\n")
if len(code_lines) > 100:
code = "\n".join(code_lines[:100])
code += f"\n\n... ({len(code_lines) - 100} more lines omitted)"
filename = parsed["filepath"].split("/")[-1]
language = get_language_from_filename(filename)
embed = discord.Embed(
title="Code Preview",
description=f"**Repository URL:** [{parsed['owner']}/{parsed['repo']}]({url})",
color=0x7289DA,
)
embed.set_author(
name="Utility", icon_url="https://yes.nighty.works/raw/8VLDcg.webp"
)
embed.add_field(
name="File", value=f"`{parsed['filepath']}`{line_info}", inline=True
)
embed.add_field(name="Branch", value=f"`{parsed['branch']}`", inline=True)
embed.set_footer(
text=f"Requested by {context.author.name}",
icon_url=context.author.display_avatar.url,
)
code_block = f"```{language}\n{code}\n```"
if len(code_block) > 1990:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.defer(ephemeral=True)
await context.channel.send(embed=embed)
else:
await context.channel.send(embed=embed)
max_code_length = 1980 - len(language) - 8
code_lines = code.split("\n")
current_chunk = []
current_length = 0
for line in code_lines:
line_length = len(line) + 1
if current_length + line_length > max_code_length:
remaining_lines = len(code_lines) - len(current_chunk)
if remaining_lines > 0:
current_chunk.append(
f"\n... ({remaining_lines} more lines omitted)"
)
chunk_text = "\n".join(current_chunk)
await context.channel.send(f"```{language}\n{chunk_text}\n```")
break
else:
current_chunk.append(line)
current_length += line_length
else:
if current_chunk:
chunk_text = "\n".join(current_chunk)
await context.channel.send(f"```{language}\n{chunk_text}\n```")
if interaction is not None:
try:
await interaction.delete_original_response()
except:
pass
else:
interaction = getattr(context, "interaction", None)
if interaction is not None:
if not interaction.response.is_done():
await interaction.response.defer(ephemeral=True)
await context.channel.send(embed=embed)
await context.channel.send(code_block)
try:
await interaction.delete_original_response()
except:
pass
else:
await context.channel.send(embed=embed)
await context.channel.send(code_block)
return codepreview

Some files were not shown because too many files have changed in this diff Show More