mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1148ba3e6e | ||
|
5498bec8cc | ||
|
9eed0c6be5 | ||
|
acf6b7cf58 | ||
|
8598419c5f | ||
|
758093eb32 | ||
|
f44dd03f1a | ||
|
8ac5ec9f57 | ||
|
5209ba802a | ||
|
adfce6670c | ||
|
f8fbc71985 | ||
|
d4e2516a17 | ||
|
f62a67e2e6 | ||
|
7895b5e702 | ||
|
28c8ccfb5f | ||
|
79026e0c27 | ||
|
8b228b840a | ||
|
f61c1a159d | ||
|
2cdcdb2b23 | ||
|
8d0a3ecb20 | ||
|
97f1405a94 | ||
|
0622236523 | ||
|
0f240925e8 | ||
|
d583e2b99a | ||
|
2d72f6f498 | ||
|
5143e42dff | ||
|
790c36df8d | ||
|
15709bc8fb | ||
|
f280f72227 | ||
|
4069368beb | ||
|
e036a2d3c9 |
159
CHANGELOG.md
159
CHANGELOG.md
@@ -2,6 +2,80 @@
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||
|
||||
## [5.3.2] - 14.12.2024
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.banner` should be working properly now with both server and global user banners
|
||||
|
||||
## [5.3.1] - 13.12.2024
|
||||
|
||||
## Changed
|
||||
|
||||
- `.translate` will now use 2 embeds, to allow for longer messages
|
||||
- Added role icon to `.inrole`, if it exists
|
||||
- `.honeypot` will now add a 'Honeypot' as a ban reason.
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.winlb` looks better, has a title, shows 9 entries now
|
||||
- `.sar ex` help updated
|
||||
- `.banner` partially fixed, it still can't show global banners, but it will show guild ones correctly, in a good enough size
|
||||
- `.sclr` will now show correct color hexes without alpha
|
||||
- `.dmcmd` will now correctly block commands in dms, not globally
|
||||
|
||||
## [5.3.0] - 10.12.2024
|
||||
|
||||
## Added
|
||||
|
||||
- Added `.minesweeper` / `.mw` command - spoiler-based minesweeper minigame. Just for fun
|
||||
- Added `.temprole` command - add a role to a user for a certain amount of time, after which the role will be removed
|
||||
- Added `.xplevelset` - you can now set a level for a user in your server
|
||||
- Added `.winlb` command - leaderboard of top gambling wins
|
||||
- Added `.notify` command
|
||||
- Specify an event to be notified about, and the bot will post the specified message in the current channel when the
|
||||
event occurs
|
||||
- A few events supported right now:
|
||||
- `UserLevelUp` when user levels up in the server
|
||||
- `AddRoleReward` when a role is added to a user through .xpreward system
|
||||
- `RemoveRoleReward` when a role is removed from a user through .xpreward system
|
||||
- `Protection` when antialt, antiraid or antispam protection is triggered
|
||||
- Added `.banner` command to see someone's banner
|
||||
- Selfhosters:
|
||||
- Added `.dmmod` and `.dmcmd` - you can now disable or enable whether commands or modules can be executed in bot's
|
||||
DMs
|
||||
|
||||
## Changed
|
||||
|
||||
- Giveaway improvements
|
||||
- Now mentions winners in a separate message
|
||||
- Shows the timestamp of when the giveaway ends
|
||||
- Xp Changes
|
||||
- Removed awarded xp (the number in the brackets on the xp card)
|
||||
- Awarded xp, (or the new level set) now directly apply to user's real xp
|
||||
- Server xp notifications are now set by the server admin/manager in a specified channel
|
||||
- `.sclr show` will now show hex code of the current color
|
||||
- Queueing a song will now restart the playback if the queue is on the last track and stopped (there were no more tracks
|
||||
to play)
|
||||
- `.translate` will now use 2 embeds instead of 1
|
||||
|
||||
## Fixed
|
||||
|
||||
- .setstream and .setactivity will now pause .ropl (rotating statuses)
|
||||
- Fixed `.sar ex` help description
|
||||
|
||||
## Removed
|
||||
|
||||
- `.xpnotify` command, superseded by `.notify`, although as of right now you can't post user's level up in the same
|
||||
channel user last typed, because you have to specify a channel where the notify messages will be posted
|
||||
|
||||
## [5.2.4] - 27.11.2024
|
||||
|
||||
## Fixed
|
||||
|
||||
- More fixes for .sclr
|
||||
- `.iamn` fixed
|
||||
|
||||
## [5.2.3] - 27.11.2024
|
||||
|
||||
## Fixed
|
||||
@@ -37,46 +111,49 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
||||
|
||||
- Added `.todo undone` command to unmark a todo as done
|
||||
- Added Button Roles!
|
||||
- `.btr a` to add a button role to the specified message
|
||||
- `.btr list` to list all button roles on the server
|
||||
- `.btr rm` to remove a button role from the specified message
|
||||
- `.btr rma` to remove all button roles on the specified message
|
||||
- `.btr excl` to toggle exclusive button roles (only 1 role per message or any number)
|
||||
- Use `.h btr` for more info
|
||||
- `.btr a` to add a button role to the specified message
|
||||
- `.btr list` to list all button roles on the server
|
||||
- `.btr rm` to remove a button role from the specified message
|
||||
- `.btr rma` to remove all button roles on the specified message
|
||||
- `.btr excl` to toggle exclusive button roles (only 1 role per message or any number)
|
||||
- Use `.h btr` for more info
|
||||
- Added `.wrongsong` which will delete the last queued song.
|
||||
- Useful in case you made a mistake, or the bot queued a wrong song
|
||||
- It will reset after a shuffle or fairplay toggle, or similar events.
|
||||
- Useful in case you made a mistake, or the bot queued a wrong song
|
||||
- It will reset after a shuffle or fairplay toggle, or similar events.
|
||||
- Added Server color Commands!
|
||||
- Every Server can now set their own colors for ok/error/pending embed (the default green/red/yellow color on the left side of the message the bot sends)
|
||||
- Use `.h .sclr` to see the list of commands
|
||||
- `.sclr show` will show the current server colors
|
||||
- `.sclr ok <color hex>` to set ok color
|
||||
- `.sclr warn <color hex>` to set warn color
|
||||
- `.sclr error <color hex>` to set error color
|
||||
- Every Server can now set their own colors for ok/error/pending embed (the default green/red/yellow color on the
|
||||
left side of the message the bot sends)
|
||||
- Use `.h .sclr` to see the list of commands
|
||||
- `.sclr show` will show the current server colors
|
||||
- `.sclr ok <color hex>` to set ok color
|
||||
- `.sclr warn <color hex>` to set warn color
|
||||
- `.sclr error <color hex>` to set error color
|
||||
|
||||
### Changed
|
||||
|
||||
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands
|
||||
- `.sar autodel`
|
||||
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
|
||||
- `.sar ad`
|
||||
- Adds a role to the list of self-assignable roles. You can also specify a group.
|
||||
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role per group.
|
||||
- `.sar groupname`
|
||||
- Sets a self assignable role group name. Provide no name to remove.
|
||||
- `.sar remove`
|
||||
- Removes a specified role from the list of self-assignable roles.
|
||||
- `.sar list`
|
||||
- Lists self-assignable roles. Shows 20 roles per page.
|
||||
- `.sar exclusive`
|
||||
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.
|
||||
- `.sar rolelvlreq`
|
||||
- Set a level requirement on a self-assignable role.
|
||||
- `.sar grouprolereq`
|
||||
- Set a role that users have to have in order to assign a self-assignable role from the specified group.
|
||||
- `.sar groupdelete`
|
||||
- Deletes a self-assignable role group
|
||||
- `.iam` and `.iamn` are unchanged
|
||||
- `.sar autodel`
|
||||
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
|
||||
- `.sar ad`
|
||||
- Adds a role to the list of self-assignable roles. You can also specify a group.
|
||||
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role
|
||||
per group.
|
||||
- `.sar groupname`
|
||||
- Sets a self assignable role group name. Provide no name to remove.
|
||||
- `.sar remove`
|
||||
- Removes a specified role from the list of self-assignable roles.
|
||||
- `.sar list`
|
||||
- Lists self-assignable roles. Shows 20 roles per page.
|
||||
- `.sar exclusive`
|
||||
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role
|
||||
per group.
|
||||
- `.sar rolelvlreq`
|
||||
- Set a level requirement on a self-assignable role.
|
||||
- `.sar grouprolereq`
|
||||
- Set a role that users have to have in order to assign a self-assignable role from the specified group.
|
||||
- `.sar groupdelete`
|
||||
- Deletes a self-assignable role group
|
||||
- `.iam` and `.iamn` are unchanged
|
||||
- Removed patron limits from Reaction Roles. Anyone can have as many reros as they like.
|
||||
- `.timely` captcha made stronger and cached per user.
|
||||
- `.bsreset` price reduced by 90%
|
||||
@@ -90,9 +167,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
||||
### Added
|
||||
|
||||
- Added `.rakeback` command, get a % of house edge back as claimable currency
|
||||
- Added `.snipe` command to quickly get a copy of a posted message as an embed
|
||||
- You can reply to a message to snipe that message
|
||||
- Or just type .snipe and the bot will snipe the last message in the channel with content or image
|
||||
- Added `.snipe` command to quickly get a copy of a posted message as an embed
|
||||
- You can reply to a message to snipe that message
|
||||
- Or just type .snipe and the bot will snipe the last message in the channel with content or image
|
||||
- Added `.betstatsreset` / `.bsreset` command to reset your stats for a fee
|
||||
- Added `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games
|
||||
- Added `.waifuclaims` command which lists all of your claimed waifus
|
||||
@@ -102,9 +179,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
||||
|
||||
- `.divorce` no longer has a cooldown
|
||||
- `.betroll` has a 2% better payout
|
||||
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
|
||||
- now has a new symbol, wheat
|
||||
- worse around 1% in total (now shares the top spot with .bf)
|
||||
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
|
||||
- now has a new symbol, wheat
|
||||
- worse around 1% in total (now shares the top spot with .bf)
|
||||
|
||||
## [5.1.19] - 04.11.2024
|
||||
|
||||
@@ -123,7 +200,7 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
||||
|
||||
- `.plant/pick` password font size will be slightly bigger
|
||||
- `.race` will now have 82-94% payout rate based on the number of players playing (1-12, x0.01 per player).
|
||||
- Any player over 12 won't increase payout
|
||||
- Any player over 12 won't increase payout
|
||||
|
||||
### Fixed
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Config
|
||||
`.config` is the new `.bce`, it gives you a fast and easy way to edit most bot settings/values. Use `.h .config` for explanation.
|
||||
`.config` gives you a fast and easy way to edit most bot settings/values. Use `.h .config` for explanation.
|
||||
|
||||
Use `.config` to see the list of editable config files
|
||||
Use `.config <config-name>` to see the list of settable properties on that config
|
||||
|
@@ -1,27 +1,30 @@
|
||||
## Creds Guide
|
||||
|
||||
This document aims to guide you through the process of creating a Discord account for your bot
|
||||
(the Discord Bot application), and inviting that account into your Discord server.
|
||||
This guide will show you how to create your own discord bot, invite it to your server, and copy it's credentials to your `creds.yml` in order to run your bot.
|
||||
|
||||
- Start by opening your creds.yml
|
||||
- If you're on a windows installer version, click on the creds button next to your bot's RUN button.
|
||||
- If you're on linux from source or windows from source version, open `nadekobot/output/creds.yml`. Please use visual studio code, notepad++ or another code editor. Usage of notepad is discouraged.
|
||||
|
||||

|
||||
|
||||
- Go to [the Discord developer application page][DiscordApp].
|
||||
- Log in with your Discord account.
|
||||
- Click **New Application**.
|
||||
- Fill out the `Name` field however you like.
|
||||
- Go to the **Bot** tab on the left sidebar.
|
||||
- Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
|
||||
- **Optional:** Add bot's avatar and description.
|
||||
- Copy your Token to `creds.yml` as shown above.
|
||||
- Scroll down to the **`Privileged Gateway Intents`** section
|
||||
- **Enable the following:**
|
||||
1. Go to [the Discord developer application page][DiscordApp].
|
||||
2. Log in with your Discord account.
|
||||
3. Click **New Application**.
|
||||
3. Fill out the `Name` field however you like, accept the terms, and confirm.
|
||||
1. Go to the **Bot** tab on the left sidebar.
|
||||
1. Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
|
||||
1. **Optional:** Add bot's avatar and description.
|
||||
1. Copy your Token to `creds.yml` as shown above.
|
||||
1. Scroll down to the **`Privileged Gateway Intents`** section
|
||||
- Enable the following:
|
||||
- **PRESENCE INTENT**
|
||||
- **SERVER MEMBERS INTENT**
|
||||
- **MESSAGE CONTENT INTENT**
|
||||
|
||||
These are required for a number of features to function properly, and all should be on.
|
||||
|
||||
##### Getting Owner ID*(s)*:
|
||||
##### Getting Owner ID
|
||||
|
||||
- Go to your Discord server and attempt to mention yourself, but put a backslash at the start
|
||||
*(to make it slightly easier, add the backslash after the mention has been typed)*.
|
||||
|
@@ -7,10 +7,9 @@ Donations go a long way in helping us keep the project alive, and we appreciate
|
||||
|
||||
Donating to us also gives you the following benefits:
|
||||
|
||||
- A hoisted **Donators role** in our [Discord server][discord-server]
|
||||
- A hoisted **Patron** role in [Nadeko Discord server][discord-server]
|
||||
- Access to exclusive **#noticed** text and voice channels
|
||||
- **1000 flowers** on the public bot per dollar donated (after fees)
|
||||
- **Expressions** on the public bot for [Patreon pledges][patreon] of $5 or higher
|
||||
- **3000 flowers** on the public bot per dollar donated (after fees)
|
||||
|
||||
## Patreon
|
||||
|
||||
@@ -26,7 +25,7 @@ You can set up a monthly pledge on [Patreon][patreon] and support the project's
|
||||
You can also donate to us through [PayPal][paypal] for one-time donations using the button below, or by donating to `nadekodiscordbot@gmail.com`.
|
||||
|
||||
!!! Note
|
||||
Mention your Discord tag (Username#1234) in the payment note to receive flower rewards.
|
||||
Mention your Discord username or user id in the payment note to receive flower rewards.
|
||||
|
||||
[![img][paypal-button]][paypal]
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
## Setting Up NadekoBot on Windows With the Updater
|
||||
|
||||
| Table of Contents|
|
||||
| :---------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Prerequisites](#prerequisites) |
|
||||
| [Setup](#setup) |
|
||||
| [Starting the Bot](#starting-the-bot) |
|
||||
| [Updating Nadeko](#updating-nadeko) |
|
||||
| :-|
|
||||
| [Prerequisites](#prerequisites) |
|
||||
| [Setup](#setup) |
|
||||
| [Starting the Bot](#starting-the-bot) |
|
||||
| [Updating Nadeko](#updating-nadeko) |
|
||||
| [Manually Installing the Prerequisites from the Updater](#music-prerequisites) |
|
||||
|
||||
*Note: If you want to make changes to Nadeko's source code, please follow the [From Source](#windows-from-source) guide instead.*
|
||||
@@ -13,7 +13,6 @@
|
||||
#### Prerequisites
|
||||
|
||||
- Windows 10 or later (64-bit)
|
||||
- [Create a Discord Bot application and invite the bot to your server](../creds-guide.md)
|
||||
|
||||
**Optional**
|
||||
|
||||
@@ -29,8 +28,7 @@
|
||||

|
||||
- Click on **`DOWNLOAD`** at the lower right
|
||||

|
||||
- **Note: Redis is optional. install Redis manually here: [Redis] Download and run the **`.msi`** file.**
|
||||
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DLP`**.
|
||||
- If you want to use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DLP`**.
|
||||
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
|
||||
- When installation is finished, click on **`CREDS`** to the left of **`RUN`** at the lower right.
|
||||
- Follow the guide on how to [Set up the creds.yml](../../creds-guide) file.
|
||||
@@ -39,8 +37,6 @@
|
||||
|
||||
- Either click on **`RUN`** button in the updater or run the bot via its desktop shortcut.
|
||||
|
||||
### If you get a "No owner channels created..." message. Please follow the creds guide again [**HERE**](../../creds-guide).
|
||||
|
||||
#### Updating Nadeko
|
||||
|
||||
- Make sure Nadeko is closed and not running
|
||||
@@ -52,71 +48,6 @@
|
||||
- Launch the bot
|
||||
- You've updated and are running again, easy as that!
|
||||
|
||||
#### Manual Prerequisite Installation
|
||||
|
||||
You can still install them manually:
|
||||
|
||||
- [Redis] (OPTIONAL) - Download and run the **`.msi`** file
|
||||
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, then just move the `ffmpeg.exe` file to NadekoBot/system
|
||||
- [youtube-dlp] - Click to download the `yt-dlp.exe` file then put `yt-dlp.exe` in a path that's in your PATH environment variable. If you don't know what that is, then just move the `yt-dlp.exe` file to NadekoBot/system
|
||||
|
||||
## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
|
||||
|
||||
### Windows From Source
|
||||
|
||||
##### Prerequisites
|
||||
|
||||
**Install these before proceeding or your bot will not work!**
|
||||
- [.net 8](https://dotnet.microsoft.com/en-us/download) - needed to compile and run the bot
|
||||
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
|
||||
- [Redis] (OPTIONAL)- to cache things needed by some features and persist through restarts
|
||||
|
||||
##### Installation Instructions
|
||||
|
||||
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and navigate to the location where you want to install the bot (for example `cd ~/Desktop/`)
|
||||
|
||||
1. `git clone https://gitlab.com/kwoth/nadekobot -b v5 --depth 1`
|
||||
2. `cd nadekobot`
|
||||
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
|
||||
4. `cd output`
|
||||
5. `cp creds_example.yml creds.yml`
|
||||
6. Open `creds.yml` with your favorite text editor (Please don't use Notepad or WordPad. You can use Notepad++, VSCode, Atom, Sublime, or something similar)
|
||||
7. [Enter your bot's token](#creds-guide)
|
||||
8. Run the bot `dotnet NadekoBot.dll`
|
||||
9. 🎉
|
||||
|
||||
##### Update Instructions
|
||||
|
||||
Open PowerShell as described above and run the following commands:
|
||||
|
||||
1. Stop the bot
|
||||
- ⚠️ Make sure you don't have your database, credentials or any other nadekobot folder open in some application, this might prevent some of the steps from executing succesfully
|
||||
2. Navigate to your bot's folder, example:
|
||||
- `cd ~/Desktop/nadekobot`
|
||||
3. Pull the new version, and make sure you're on the v5 branch
|
||||
- *⚠️ the first 3 lines can be omitted if you're already on v5. If you're updating from v4, you must run them*
|
||||
- `git remote set-branches origin '*'`
|
||||
- `git fetch -v --depth=1`
|
||||
- `git checkout v5`
|
||||
- `git pull`
|
||||
- ⚠️ If this fails, you may want to stash or remove your code changes if you don't know how to resolve merge conflicts
|
||||
4. **Backup** old output in case your data is overwritten
|
||||
- `cp -r -fo output/ output-old`
|
||||
5. Build the bot again
|
||||
- `dotnet publish -c Release -o output/ src/NadekoBot/`
|
||||
6. Remove old strings and aliases to avoid overwriting the updated versions of those files
|
||||
- ⚠ If you've modified said files, back them up instead
|
||||
- `rm output-old/data/aliases.yml`
|
||||
- `rm -r output-old/data/strings`
|
||||
7. Copy old data
|
||||
- `cp -Recurse .\output-old\data\ .\output\ -Force`
|
||||
8. Copy creds.yml
|
||||
- `cp output-old/creds.yml output/`
|
||||
9. Run the bot
|
||||
- `cd output`
|
||||
- `dotnet NadekoBot.dll`
|
||||
|
||||
🎉 Enjoy
|
||||
|
||||
#### Music prerequisites
|
||||
In order to use music commands, you need ffmpeg and yt-dlp installed.
|
||||
@@ -131,4 +62,4 @@ In order to use music commands, you need ffmpeg and yt-dlp installed.
|
||||
[Visual C++ 2017 (x64)]: https://aka.ms/vs/15/release/vc_redist.x64.exe
|
||||
[ffmpeg-32bit]: https://cdn.nadeko.bot/dl/ffmpeg-32.zip
|
||||
[ffmpeg-64bit]: https://cdn.nadeko.bot/dl/ffmpeg-64.zip
|
||||
[youtube-dlp]: https://github.com/yt-dlp/yt-dlp/releases
|
||||
[youtube-dlp]: https://github.com/yt-dlp/yt-dlp/releases
|
77
docs/guides/windows-source-guide.md
Normal file
77
docs/guides/windows-source-guide.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## Setting Up NadekoBot on Windows from source
|
||||
|
||||
1. Prerequisites
|
||||
|
||||
- Windows 10 or later (64-bit)
|
||||
- [.net 8 sdk](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
- If you want nadeko to play music: [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required, you may install them later)
|
||||
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
|
||||
- **Optional** Any code editor, for example [Visual Studio Code](https://code.visualstudio.com/Download)
|
||||
- You'll need to at least modify creds.yml, notepad is inadequate
|
||||
|
||||
|
||||
##### Installation Instructions
|
||||
|
||||
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and
|
||||
|
||||
|
||||
0. Navigate to the location where you want to install the bot
|
||||
- for example, type `cd ~/Desktop/` and press enter
|
||||
1. `git clone https://gitlab.com/kwoth/nadekobot -b v5 --depth 1`
|
||||
2. `cd nadekobot`
|
||||
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
|
||||
4. `cd output`
|
||||
5. `cp creds_example.yml creds.yml`
|
||||
6. "You're done installing, you may now proceed to set up your bot's credentials by following the [#creds-guide]
|
||||
- Once done, come back here and run the last command
|
||||
8. Run the bot `dotnet NadekoBot.dll`
|
||||
9. 🎉 Enjoy
|
||||
|
||||
##### Update Instructions
|
||||
|
||||
Open PowerShell as described above and run the following commands:
|
||||
|
||||
1. Stop the bot
|
||||
- ⚠️ Make sure you don't have your database, credentials or any other nadekobot folder open in some application, this might prevent some of the steps from executing succesfully
|
||||
2. Navigate to your bot's folder, example:
|
||||
- `cd ~/Desktop/nadekobot`
|
||||
3. Pull the new version, and make sure you're on the v5 branch
|
||||
- *⚠️ If you're on v4, you must run these commands, if not, you may skip them.*
|
||||
- `git remote set-branches origin '*'`
|
||||
- `git fetch -v --depth=1`
|
||||
- `git checkout v5`
|
||||
- `git pull`
|
||||
- ⚠️ If this fails, you may want to stash or remove your code changes if you don't know how to resolve merge conflicts
|
||||
4. **Backup** old output in case your data is overwritten
|
||||
- `cp -r -fo output/ output-old`
|
||||
5. Build the bot again
|
||||
- `dotnet publish -c Release -o output/ src/NadekoBot/`
|
||||
6. Remove old strings and aliases to avoid overwriting the updated versions of those files
|
||||
- ⚠ If you've modified said files, back them up instead
|
||||
- `rm output-old/data/aliases.yml`
|
||||
- `rm -r output-old/data/strings`
|
||||
7. Copy old data, and new strings
|
||||
- `cp -Recurse -Force .\output-old\data\ .\output\`
|
||||
- `cp -Recurse -Force src/NadekoBot/data/strings/ output/data/`
|
||||
8. Copy creds.yml
|
||||
- `cp output-old/creds.yml output/`
|
||||
9. Run the bot
|
||||
- `cd output`
|
||||
- `dotnet NadekoBot.dll`
|
||||
|
||||
🎉 Enjoy
|
||||
|
||||
#### Music prerequisites
|
||||
In order to use music commands, you need ffmpeg and yt-dlp installed.
|
||||
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `ffmpeg.exe` file to `NadekoBot/output`.
|
||||
- [youtube-dlp] - Click to download the `yt-dlp.exe` file, then move `yt-dlp.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `yt-dlp.exe` file to `NadekoBot/system`.
|
||||
|
||||
[Updater]: https://dl.nadeko.bot/v3/
|
||||
[Notepad++]: https://notepad-plus-plus.org/
|
||||
[.net]: https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
[Redis]: https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.msi
|
||||
[Visual C++ 2010 (x86)]: https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe
|
||||
[Visual C++ 2017 (x64)]: https://aka.ms/vs/15/release/vc_redist.x64.exe
|
||||
[ffmpeg-32bit]: https://cdn.nadeko.bot/dl/ffmpeg-32.zip
|
||||
[ffmpeg-64bit]: https://cdn.nadeko.bot/dl/ffmpeg-64.zip
|
||||
[youtube-dlp]: https://github.com/yt-dlp/yt-dlp/releases
|
@@ -16,6 +16,7 @@ To self-host your own Nadeko, use the guides below:
|
||||
- [:material-microsoft-windows: Windows guide][windows-guide]
|
||||
- [:material-linux: Linux guide][linux-guide]
|
||||
- [:material-apple: Mac OS guide][macos-guide]
|
||||
- [:material-microsoft-windows: Windows (from source) guide][windows-source-guide]
|
||||
|
||||
In case you need any help, join our [Discord server][discord-server] where we may provide support.
|
||||
|
||||
@@ -32,6 +33,7 @@ If you're unsure whether something is an issue, ask in our support server first.
|
||||
[invite]: https://invite.nadeko.bot/
|
||||
[commands]: https://nadeko.bot/commands/
|
||||
[windows-guide]: ./guides/windows-guide.md
|
||||
[windows-source-guide]: ./guides/windows-source-guide.md
|
||||
[linux-guide]: ./guides/linux-guide.md
|
||||
[macos-guide]: ./guides/osx-guide.md
|
||||
[from-source-guide]: ./guides/from-source.md
|
||||
|
@@ -75,6 +75,7 @@ nav:
|
||||
- Windows Guide: guides/windows-guide.md
|
||||
- Linux Guide: guides/linux-guide.md
|
||||
- OSX Guide: guides/osx-guide.md
|
||||
- Windows Guide (from source): guides/windows-source-guide.md
|
||||
- Docker Guide (unsupported): guides/docker-guide.md
|
||||
- Commands:
|
||||
- Readme: commands-readme.md
|
||||
|
@@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
|
||||
<PackageReference Include="Discord.Net.Core" Version="3.16.0" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="15.1.4" />
|
||||
</ItemGroup>
|
||||
|
@@ -357,3 +357,4 @@ resharper_arrange_redundant_parentheses_highlighting = hint
|
||||
# IDE0011: Add braces
|
||||
dotnet_diagnostic.IDE0011.severity = warning
|
||||
|
||||
resharper_arrange_type_member_modifiers_highlighting = hint
|
@@ -20,7 +20,6 @@ public static class UserXpExtensions
|
||||
{
|
||||
Xp = 0,
|
||||
UserId = userId,
|
||||
NotifyOnLevelUp = XpNotificationLocation.None,
|
||||
GuildId = guildId
|
||||
});
|
||||
}
|
||||
@@ -31,17 +30,17 @@ public static class UserXpExtensions
|
||||
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
|
||||
=> await xps.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||
.OrderByDescending(x => x.Xp)
|
||||
.Take(count)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
|
||||
=> await xps.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.Xp + x.AwardedXp
|
||||
&& x.Xp
|
||||
> xps.AsQueryable()
|
||||
.Where(y => y.UserId == userId && y.GuildId == guildId)
|
||||
.Select(y => y.Xp + y.AwardedXp)
|
||||
.Select(y => y.Xp)
|
||||
.FirstOrDefault())
|
||||
.CountAsyncLinqToDB()
|
||||
+ 1;
|
||||
@@ -53,6 +52,6 @@ public static class UserXpExtensions
|
||||
=> await userXp
|
||||
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||
.FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs
|
||||
? new(uxs.Xp + uxs.AwardedXp)
|
||||
? new(uxs.Xp)
|
||||
: new(0);
|
||||
}
|
@@ -4,38 +4,28 @@ namespace NadekoBot.Db;
|
||||
|
||||
public readonly struct LevelStats
|
||||
{
|
||||
public const int XP_REQUIRED_LVL_1 = 36;
|
||||
|
||||
public long Level { get; }
|
||||
public long LevelXp { get; }
|
||||
public long RequiredXp { get; }
|
||||
public long TotalXp { get; }
|
||||
|
||||
public LevelStats(long xp)
|
||||
public LevelStats(long totalXp)
|
||||
{
|
||||
if (xp < 0)
|
||||
xp = 0;
|
||||
if (totalXp < 0)
|
||||
totalXp = 0;
|
||||
|
||||
TotalXp = xp;
|
||||
|
||||
const int baseXp = XP_REQUIRED_LVL_1;
|
||||
|
||||
var required = baseXp;
|
||||
var totalXp = 0;
|
||||
var lvl = 1;
|
||||
while (true)
|
||||
{
|
||||
required = (int)(baseXp + (baseXp / 4.0 * (lvl - 1)));
|
||||
|
||||
if (required + totalXp > xp)
|
||||
break;
|
||||
|
||||
totalXp += required;
|
||||
lvl++;
|
||||
}
|
||||
|
||||
Level = lvl - 1;
|
||||
LevelXp = xp - totalXp;
|
||||
RequiredXp = required;
|
||||
TotalXp = totalXp;
|
||||
Level = GetLevelByTotalXp(totalXp);
|
||||
LevelXp = totalXp - GetTotalXpReqForLevel(Level);
|
||||
RequiredXp = (9 * (Level + 1)) + 27;
|
||||
}
|
||||
|
||||
public static LevelStats CreateForLevel(long level)
|
||||
=> new(GetTotalXpReqForLevel(level));
|
||||
|
||||
public static long GetTotalXpReqForLevel(long level)
|
||||
=> ((9 * level * level) + (63 * level)) / 2;
|
||||
|
||||
public static long GetLevelByTotalXp(long totalXp)
|
||||
=> (long)((-7.0 / 2) + (1 / 6.0 * Math.Sqrt((8 * totalXp) + 441)));
|
||||
}
|
@@ -36,29 +36,37 @@ public class GuildConfig : DbEntity
|
||||
public HashSet<FilterChannelId> FilterInvitesChannelIds { get; set; } = new();
|
||||
public HashSet<FilterLinksChannelId> FilterLinksChannelIds { get; set; } = new();
|
||||
|
||||
//public bool FilterLinks { get; set; }
|
||||
//public HashSet<FilterLinksChannelId> FilterLinksChannels { get; set; } = new HashSet<FilterLinksChannelId>();
|
||||
|
||||
public bool FilterWords { get; set; }
|
||||
public HashSet<FilteredWord> FilteredWords { get; set; } = new();
|
||||
public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new();
|
||||
|
||||
// mute
|
||||
public HashSet<MutedUserId> MutedUsers { get; set; } = new();
|
||||
|
||||
public string MuteRoleName { get; set; }
|
||||
|
||||
// chatterbot
|
||||
public bool CleverbotEnabled { get; set; }
|
||||
|
||||
// protection
|
||||
public AntiRaidSetting AntiRaidSetting { get; set; }
|
||||
public AntiSpamSetting AntiSpamSetting { get; set; }
|
||||
public AntiAltSetting AntiAltSetting { get; set; }
|
||||
|
||||
// time
|
||||
public string Locale { get; set; }
|
||||
public string TimeZoneId { get; set; }
|
||||
|
||||
|
||||
// timers
|
||||
public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new();
|
||||
public HashSet<UnbanTimer> UnbanTimer { get; set; } = new();
|
||||
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
|
||||
|
||||
// vcrole
|
||||
public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
|
||||
|
||||
// aliases
|
||||
public HashSet<CommandAlias> CommandAliases { get; set; } = new();
|
||||
public bool WarningsInitialized { get; set; }
|
||||
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
||||
|
24
src/NadekoBot/Db/Models/Notify.cs
Normal file
24
src/NadekoBot/Db/Models/Notify.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace NadekoBot.Db.Models;
|
||||
|
||||
public class Notify
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public NotifyType Type { get; set; }
|
||||
|
||||
[MaxLength(10_000)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public enum NotifyType
|
||||
{
|
||||
LevelUp = 0,
|
||||
Protection = 1, Prot = 1,
|
||||
AddRoleReward = 2,
|
||||
RemoveRoleReward = 3,
|
||||
}
|
12
src/NadekoBot/Db/Models/roles/TempRole.cs
Normal file
12
src/NadekoBot/Db/Models/roles/TempRole.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace NadekoBot.Db.Models;
|
||||
|
||||
public class TempRole
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public ulong GuildId { get; set; }
|
||||
public bool Remove { get; set; }
|
||||
public ulong RoleId { get; set; }
|
||||
public ulong UserId { get; set; }
|
||||
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
@@ -6,6 +6,4 @@ public class UserXpStats : DbEntity
|
||||
public ulong UserId { get; set; }
|
||||
public ulong GuildId { get; set; }
|
||||
public long Xp { get; set; }
|
||||
public long AwardedXp { get; set; }
|
||||
public XpNotificationLocation NotifyOnLevelUp { get; set; }
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
namespace NadekoBot.Db.Models;
|
||||
namespace NadekoBot.Db.Models;
|
||||
|
||||
public enum XpNotificationLocation
|
||||
{
|
||||
|
@@ -74,6 +74,35 @@ public abstract class NadekoContext : DbContext
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
#region Notify
|
||||
|
||||
modelBuilder.Entity<Notify>(e =>
|
||||
{
|
||||
e.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
Event = x.Type
|
||||
});
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region TempRoles
|
||||
|
||||
modelBuilder.Entity<TempRole>(e =>
|
||||
{
|
||||
e.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.UserId,
|
||||
x.RoleId
|
||||
});
|
||||
|
||||
e.HasIndex(x => x.ExpiresAt);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region GuildColors
|
||||
|
||||
modelBuilder.Entity<GuildColors>()
|
||||
@@ -135,13 +164,18 @@ public abstract class NadekoContext : DbContext
|
||||
|
||||
#region UserBetStats
|
||||
|
||||
modelBuilder.Entity<UserBetStats>()
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.Game
|
||||
})
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<UserBetStats>(ubs =>
|
||||
{
|
||||
ubs.HasIndex(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.Game
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
ubs.HasIndex(x => x.MaxWin)
|
||||
.IsUnique(false);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -449,7 +483,6 @@ public abstract class NadekoContext : DbContext
|
||||
xps.HasIndex(x => x.UserId);
|
||||
xps.HasIndex(x => x.GuildId);
|
||||
xps.HasIndex(x => x.Xp);
|
||||
xps.HasIndex(x => x.AwardedXp);
|
||||
|
||||
#endregion
|
||||
|
||||
|
@@ -5,6 +5,16 @@ namespace NadekoBot.Migrations;
|
||||
|
||||
public static class MigrationQueries
|
||||
{
|
||||
public static void MergeAwardedXp(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE UserXpStats
|
||||
SET Xp = AwardedXp + Xp,
|
||||
AwardedXp = 0
|
||||
WHERE AwardedXp > 0;
|
||||
""");
|
||||
}
|
||||
|
||||
public static void MigrateSar(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
|
4117
src/NadekoBot/Migrations/PostgreSql/20241207150050_awardedxp-temprole-notify.Designer.cs
generated
Normal file
4117
src/NadekoBot/Migrations/PostgreSql/20241207150050_awardedxp-temprole-notify.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.Migrations.PostgreSql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class awardedxptemprolenotify : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_userxpstats_awardedxp",
|
||||
table: "userxpstats");
|
||||
|
||||
MigrationQueries.MergeAwardedXp(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "awardedxp",
|
||||
table: "userxpstats");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "notifyonlevelup",
|
||||
table: "userxpstats");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notify",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notify", x => x.id);
|
||||
table.UniqueConstraint("ak_notify_guildid_type", x => new { x.guildid, x.type });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "temprole",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
remove = table.Column<bool>(type: "boolean", nullable: false),
|
||||
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
expiresat = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_temprole", x => x.id);
|
||||
table.UniqueConstraint("ak_temprole_guildid_userid_roleid", x => new { x.guildid, x.userid, x.roleid });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userbetstats_maxwin",
|
||||
table: "userbetstats",
|
||||
column: "maxwin");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_temprole_expiresat",
|
||||
table: "temprole",
|
||||
column: "expiresat");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "notify");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "temprole");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_userbetstats_maxwin",
|
||||
table: "userbetstats");
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "awardedxp",
|
||||
table: "userxpstats",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "notifyonlevelup",
|
||||
table: "userxpstats",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userxpstats_awardedxp",
|
||||
table: "userxpstats",
|
||||
column: "awardedxp");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1817,6 +1817,42 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.ToTable("expressions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("ChannelId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("channelid");
|
||||
|
||||
b.Property<decimal>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10000)
|
||||
.HasColumnType("character varying(10000)")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notify");
|
||||
|
||||
b.HasAlternateKey("GuildId", "Type")
|
||||
.HasName("ak_notify_guildid_type");
|
||||
|
||||
b.ToTable("notify", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
||||
{
|
||||
b.Property<decimal>("UserId")
|
||||
@@ -2702,6 +2738,47 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.ToTable("streamrolewhitelisteduser", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.TempRole", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("expiresat");
|
||||
|
||||
b.Property<decimal>("GuildId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<bool>("Remove")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("remove");
|
||||
|
||||
b.Property<decimal>("RoleId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("roleid");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_temprole");
|
||||
|
||||
b.HasAlternateKey("GuildId", "UserId", "RoleId")
|
||||
.HasName("ak_temprole_guildid_userid_roleid");
|
||||
|
||||
b.HasIndex("ExpiresAt")
|
||||
.HasDatabaseName("ix_temprole_expiresat");
|
||||
|
||||
b.ToTable("temprole", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.TodoModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -2858,10 +2935,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<long>("AwardedXp")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("awardedxp");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
@@ -2870,10 +2943,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("guildid");
|
||||
|
||||
b.Property<int>("NotifyOnLevelUp")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("notifyonlevelup");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
@@ -2885,9 +2954,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userxpstats");
|
||||
|
||||
b.HasIndex("AwardedXp")
|
||||
.HasDatabaseName("ix_userxpstats_awardedxp");
|
||||
|
||||
b.HasIndex("GuildId")
|
||||
.HasDatabaseName("ix_userxpstats_guildid");
|
||||
|
||||
@@ -3416,6 +3482,9 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userbetstats");
|
||||
|
||||
b.HasIndex("MaxWin")
|
||||
.HasDatabaseName("ix_userbetstats_maxwin");
|
||||
|
||||
b.HasIndex("UserId", "Game")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_userbetstats_userid_game");
|
||||
|
3173
src/NadekoBot/Migrations/Sqlite/20241207150041_awardedxp-temprole-notify.Designer.cs
generated
Normal file
3173
src/NadekoBot/Migrations/Sqlite/20241207150041_awardedxp-temprole-notify.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class awardedxptemprolenotify : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_UserXpStats_AwardedXp",
|
||||
table: "UserXpStats");
|
||||
|
||||
MigrationQueries.MergeAwardedXp(migrationBuilder);
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AwardedXp",
|
||||
table: "UserXpStats");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NotifyOnLevelUp",
|
||||
table: "UserXpStats");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notify",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notify", x => x.Id);
|
||||
table.UniqueConstraint("AK_Notify_GuildId_Type", x => new { x.GuildId, x.Type });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TempRole",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Remove = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TempRole", x => x.Id);
|
||||
table.UniqueConstraint("AK_TempRole_GuildId_UserId_RoleId", x => new { x.GuildId, x.UserId, x.RoleId });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserBetStats_MaxWin",
|
||||
table: "UserBetStats",
|
||||
column: "MaxWin");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TempRole_ExpiresAt",
|
||||
table: "TempRole",
|
||||
column: "ExpiresAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notify");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TempRole");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_UserBetStats_MaxWin",
|
||||
table: "UserBetStats");
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "AwardedXp",
|
||||
table: "UserXpStats",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "NotifyOnLevelUp",
|
||||
table: "UserXpStats",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserXpStats_AwardedXp",
|
||||
table: "UserXpStats",
|
||||
column: "AwardedXp");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1356,6 +1356,33 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("Expressions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("GuildId", "Type");
|
||||
|
||||
b.ToTable("Notify");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
||||
{
|
||||
b.Property<ulong>("UserId")
|
||||
@@ -2013,6 +2040,36 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("StreamRoleWhitelistedUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.TempRole", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Remove")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("RoleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("GuildId", "UserId", "RoleId");
|
||||
|
||||
b.HasIndex("ExpiresAt");
|
||||
|
||||
b.ToTable("TempRole");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Db.Models.TodoModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -2127,18 +2184,12 @@ namespace NadekoBot.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("AwardedXp")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("NotifyOnLevelUp")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -2147,8 +2198,6 @@ namespace NadekoBot.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AwardedXp");
|
||||
|
||||
b.HasIndex("GuildId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
@@ -2541,6 +2590,8 @@ namespace NadekoBot.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MaxWin");
|
||||
|
||||
b.HasIndex("UserId", "Game")
|
||||
.IsUnique();
|
||||
|
||||
|
@@ -46,7 +46,7 @@ public partial class Administration : NadekoModule<AdministrationService>
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(GuildPerm.ManageGuild)]
|
||||
public async Task ImageOnlyChannel(StoopidTime time = null)
|
||||
public async Task ImageOnlyChannel(ParsedTimespan timespan = null)
|
||||
{
|
||||
var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
|
||||
if (newValue)
|
||||
@@ -59,7 +59,7 @@ public partial class Administration : NadekoModule<AdministrationService>
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(GuildPerm.ManageGuild)]
|
||||
public async Task LinkOnlyChannel(StoopidTime time = null)
|
||||
public async Task LinkOnlyChannel(ParsedTimespan timespan = null)
|
||||
{
|
||||
var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
|
||||
if (newValue)
|
||||
@@ -72,10 +72,10 @@ public partial class Administration : NadekoModule<AdministrationService>
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(ChannelPerm.ManageChannels)]
|
||||
[BotPerm(ChannelPerm.ManageChannels)]
|
||||
public async Task Slowmode(StoopidTime time = null)
|
||||
public async Task Slowmode(ParsedTimespan timespan = null)
|
||||
{
|
||||
var seconds = (int?)time?.Time.TotalSeconds ?? 0;
|
||||
if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6)))
|
||||
var seconds = (int?)timespan?.Time.TotalSeconds ?? 0;
|
||||
if (timespan is not null && (timespan.Time < TimeSpan.FromSeconds(0) || timespan.Time > TimeSpan.FromHours(6)))
|
||||
return;
|
||||
|
||||
await ((ITextChannel)ctx.Channel).ModifyAsync(tcp =>
|
||||
@@ -298,18 +298,18 @@ public partial class Administration : NadekoModule<AdministrationService>
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(ChannelPerm.ManageMessages)]
|
||||
[BotPerm(ChannelPerm.ManageMessages)]
|
||||
public Task Delete(ulong messageId, StoopidTime time = null)
|
||||
=> Delete((ITextChannel)ctx.Channel, messageId, time);
|
||||
public Task Delete(ulong messageId, ParsedTimespan timespan = null)
|
||||
=> Delete((ITextChannel)ctx.Channel, messageId, timespan);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null)
|
||||
=> await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync());
|
||||
public async Task Delete(ITextChannel channel, ulong messageId, ParsedTimespan timespan = null)
|
||||
=> await InternalMessageAction(channel, messageId, timespan, msg => msg.DeleteAsync());
|
||||
|
||||
private async Task InternalMessageAction(
|
||||
ITextChannel channel,
|
||||
ulong messageId,
|
||||
StoopidTime time,
|
||||
ParsedTimespan timespan,
|
||||
Func<IMessage, Task> func)
|
||||
{
|
||||
var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
|
||||
@@ -334,13 +334,13 @@ public partial class Administration : NadekoModule<AdministrationService>
|
||||
return;
|
||||
}
|
||||
|
||||
if (time is null)
|
||||
if (timespan is null)
|
||||
await msg.DeleteAsync();
|
||||
else if (time.Time <= TimeSpan.FromDays(7))
|
||||
else if (timespan.Time <= TimeSpan.FromDays(7))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(time.Time);
|
||||
await Task.Delay(timespan.Time);
|
||||
await msg.DeleteAsync();
|
||||
});
|
||||
}
|
||||
|
@@ -71,7 +71,7 @@ public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoC
|
||||
try
|
||||
{
|
||||
Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id);
|
||||
await user.BanAsync(pruneDays: 1);
|
||||
await user.BanAsync(pruneDays: 1, reason: "Honeypot");
|
||||
await user.Guild.RemoveBanAsync(user.Id);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@@ -72,18 +72,18 @@ public partial class Administration
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
|
||||
[Priority(1)]
|
||||
public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
|
||||
public async Task Mute(ParsedTimespan timespan, IGuildUser user, [Leftover] string reason = "")
|
||||
{
|
||||
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
|
||||
if (timespan.Time < TimeSpan.FromMinutes(1) || timespan.Time > TimeSpan.FromDays(49))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
|
||||
return;
|
||||
|
||||
await _service.TimedMute(user, ctx.User, time.Time, reason: reason);
|
||||
await _service.TimedMute(user, ctx.User, timespan.Time, reason: reason);
|
||||
await Response().Confirm(strs.user_muted_time(Format.Bold(user.ToString()),
|
||||
(int)time.Time.TotalMinutes)).SendAsync();
|
||||
(int)timespan.Time.TotalMinutes)).SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -133,18 +133,18 @@ public partial class Administration
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[Priority(1)]
|
||||
public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
|
||||
public async Task ChatMute(ParsedTimespan timespan, IGuildUser user, [Leftover] string reason = "")
|
||||
{
|
||||
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
|
||||
if (timespan.Time < TimeSpan.FromMinutes(1) || timespan.Time > TimeSpan.FromDays(49))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
|
||||
return;
|
||||
|
||||
await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason);
|
||||
await _service.TimedMute(user, ctx.User, timespan.Time, MuteType.Chat, reason);
|
||||
await Response().Confirm(strs.user_chat_mute_time(Format.Bold(user.ToString()),
|
||||
(int)time.Time.TotalMinutes)).SendAsync();
|
||||
(int)timespan.Time.TotalMinutes)).SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -193,18 +193,18 @@ public partial class Administration
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.MuteMembers)]
|
||||
[Priority(1)]
|
||||
public async Task VoiceMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "")
|
||||
public async Task VoiceMute(ParsedTimespan timespan, IGuildUser user, [Leftover] string reason = "")
|
||||
{
|
||||
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49))
|
||||
if (timespan.Time < TimeSpan.FromMinutes(1) || timespan.Time > TimeSpan.FromDays(49))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
|
||||
return;
|
||||
|
||||
await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason);
|
||||
await _service.TimedMute(user, ctx.User, timespan.Time, MuteType.Voice, reason);
|
||||
await Response().Confirm(strs.user_voice_mute_time(Format.Bold(user.ToString()),
|
||||
(int)time.Time.TotalMinutes)).SendAsync();
|
||||
(int)timespan.Time.TotalMinutes)).SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
23
src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs
Normal file
23
src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Collections;
|
||||
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public interface INotifyModel
|
||||
{
|
||||
static abstract string KeyName { get; }
|
||||
static abstract NotifyType NotifyType { get; }
|
||||
IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements();
|
||||
|
||||
public virtual bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public interface INotifySubscriber
|
||||
{
|
||||
Task NotifyAsync<T>(T data, bool isShardLocal = false)
|
||||
where T : struct, INotifyModel;
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Administration;
|
||||
|
||||
namespace NadekoBot.Modules.Xp.Services;
|
||||
|
||||
public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel
|
||||
{
|
||||
public static string KeyName
|
||||
=> "notify.reward.addrole";
|
||||
|
||||
public static NotifyType NotifyType
|
||||
=> NotifyType.AddRoleReward;
|
||||
|
||||
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||
{
|
||||
var model = this;
|
||||
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||
{
|
||||
{ "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() },
|
||||
{ "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() },
|
||||
{ "%event.level%", g => model.Level.ToString() }
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = UserId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = GuildId;
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public record struct LevelUpNotifyModel(
|
||||
ulong GuildId,
|
||||
ulong ChannelId,
|
||||
ulong UserId,
|
||||
long Level) : INotifyModel
|
||||
{
|
||||
public static string KeyName
|
||||
=> "notify.levelup";
|
||||
|
||||
public static NotifyType NotifyType
|
||||
=> NotifyType.LevelUp;
|
||||
|
||||
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||
{
|
||||
var data = this;
|
||||
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||
{
|
||||
{ "%event.level%", g => data.Level.ToString() },
|
||||
{ "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() },
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = GuildId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = UserId;
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Services;
|
||||
|
||||
public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel
|
||||
{
|
||||
public static string KeyName
|
||||
=> "notify.protection";
|
||||
|
||||
public static NotifyType NotifyType
|
||||
=> NotifyType.Protection;
|
||||
|
||||
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||
{
|
||||
var data = this;
|
||||
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||
{
|
||||
{ "%event.type%", g => data.ProtType.ToString() },
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = UserId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = GuildId;
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Administration;
|
||||
|
||||
namespace NadekoBot.Modules.Xp.Services;
|
||||
|
||||
public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel
|
||||
{
|
||||
public static string KeyName
|
||||
=> "notify.reward.removerole";
|
||||
|
||||
public static NotifyType NotifyType
|
||||
=> NotifyType.RemoveRoleReward;
|
||||
|
||||
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||
{
|
||||
var model = this;
|
||||
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||
{
|
||||
{ "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() },
|
||||
{ "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() },
|
||||
{ "%event.level%", g => model.Level.ToString() },
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryGetUserId(out ulong userId)
|
||||
{
|
||||
userId = UserId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetGuildId(out ulong guildId)
|
||||
{
|
||||
guildId = GuildId;
|
||||
return true;
|
||||
}
|
||||
}
|
114
src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs
Normal file
114
src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Text;
|
||||
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public partial class Administration
|
||||
{
|
||||
public class NotifyCommands : NadekoModule<NotifyService>
|
||||
{
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task Notify()
|
||||
{
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(Enum.GetValues<NotifyType>())
|
||||
.PageSize(5)
|
||||
.Page((items, page) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_available));
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
eb.AddField(item.ToString(), GetText(GetDescription(item)), false);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private LocStr GetDescription(NotifyType item)
|
||||
=> item switch
|
||||
{
|
||||
NotifyType.LevelUp => strs.notify_desc_levelup,
|
||||
NotifyType.Protection => strs.notify_desc_protection,
|
||||
NotifyType.AddRoleReward => strs.notify_desc_addrolerew,
|
||||
NotifyType.RemoveRoleReward => strs.notify_desc_removerolerew,
|
||||
_ => strs.notify_desc_not_found
|
||||
};
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task Notify(NotifyType nType, [Leftover] string? message = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
// show msg
|
||||
var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType);
|
||||
if (conf is null)
|
||||
{
|
||||
await Response().Confirm(strs.notify_msg_not_set).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.notify_msg))
|
||||
.WithDescription(conf.Message.TrimTo(2048))
|
||||
.AddField(GetText(strs.notify_type), conf.Type.ToString(), true)
|
||||
.AddField(GetText(strs.channel),
|
||||
$"""
|
||||
<#{conf.ChannelId}>
|
||||
`{conf.ChannelId}`
|
||||
""",
|
||||
true);
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message);
|
||||
await Response().Confirm(strs.notify_on($"<#{ctx.Channel.Id}>", Format.Bold(nType.ToString()))).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task NotifyList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
var notifs = await _service.GetForGuildAsync(ctx.Guild.Id);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var notif in notifs)
|
||||
{
|
||||
sb.AppendLine($"""
|
||||
- **{notif.Type}**
|
||||
<#{notif.ChannelId}> `{notif.ChannelId}`
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
if (notifs.Count == 0)
|
||||
sb.AppendLine(GetText(strs.notify_none));
|
||||
|
||||
await Response()
|
||||
.Confirm(GetText(strs.notify_list), text: sb.ToString())
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task NotifyClear(NotifyType nType)
|
||||
{
|
||||
await _service.DisableAsync(ctx.Guild.Id, nType);
|
||||
await Response().Confirm(strs.notify_off(nType)).SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public static class NotifyKeys
|
||||
{
|
||||
public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public static class NotifyModelExtensions
|
||||
{
|
||||
public static TypedKey<T> GetTypedKey<T>(this T model)
|
||||
where T : struct, INotifyModel
|
||||
=> new(T.KeyName);
|
||||
}
|
226
src/NadekoBot/Modules/Administration/Notify/NotifyService.cs
Normal file
226
src/NadekoBot/Modules/Administration/Notify/NotifyService.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Generators;
|
||||
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public sealed class NotifyService : IReadyExecutor, INotifySubscriber, INService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly IMessageSenderService _mss;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCreds _creds;
|
||||
private readonly IReplacementService _repSvc;
|
||||
private readonly IPubSub _pubSub;
|
||||
private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new();
|
||||
|
||||
public NotifyService(
|
||||
DbService db,
|
||||
IMessageSenderService mss,
|
||||
DiscordSocketClient client,
|
||||
IBotCreds creds,
|
||||
IReplacementService repSvc,
|
||||
IPubSub pubSub)
|
||||
{
|
||||
_db = db;
|
||||
_mss = mss;
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_repSvc = repSvc;
|
||||
_pubSub = pubSub;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
_events = (await uow.GetTable<Notify>()
|
||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
|
||||
_creds.TotalShards,
|
||||
_client.ShardId))
|
||||
.ToListAsyncLinqToDB())
|
||||
.GroupBy(x => x.Type)
|
||||
.ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
|
||||
.ToConcurrent();
|
||||
|
||||
|
||||
await SubscribeToEvent<LevelUpNotifyModel>();
|
||||
}
|
||||
|
||||
private async Task SubscribeToEvent<T>()
|
||||
where T : struct, INotifyModel
|
||||
{
|
||||
await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model));
|
||||
}
|
||||
|
||||
public async Task NotifyAsync<T>(T data, bool isShardLocal = false)
|
||||
where T : struct, INotifyModel
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isShardLocal)
|
||||
{
|
||||
await OnEvent(data);
|
||||
return;
|
||||
}
|
||||
|
||||
await _pubSub.Pub(data.GetTypedKey(), data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}",
|
||||
T.KeyName,
|
||||
data);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnEvent<T>(T model)
|
||||
where T : struct, INotifyModel
|
||||
{
|
||||
if (_events.TryGetValue(T.NotifyType, out var subs))
|
||||
{
|
||||
if (model.TryGetGuildId(out var gid))
|
||||
{
|
||||
if (!subs.TryGetValue(gid, out var conf))
|
||||
return;
|
||||
|
||||
await HandleNotifyEvent(conf, model);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var key in subs.Keys.ToArray())
|
||||
{
|
||||
if (subs.TryGetValue(key, out var notif))
|
||||
{
|
||||
try
|
||||
{
|
||||
await HandleNotifyEvent(notif, model);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
|
||||
T.NotifyType,
|
||||
key,
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleNotifyEvent(Notify conf, INotifyModel model)
|
||||
{
|
||||
var guild = _client.GetGuild(conf.GuildId);
|
||||
var channel = guild?.GetTextChannel(conf.ChannelId);
|
||||
|
||||
if (guild is null || channel is null)
|
||||
return;
|
||||
|
||||
IUser? user = null;
|
||||
if (model.TryGetUserId(out var userId))
|
||||
{
|
||||
user = guild.GetUser(userId) ?? _client.GetUser(userId);
|
||||
}
|
||||
|
||||
var rctx = new ReplacementContext(guild: guild, channel: channel, user: user);
|
||||
|
||||
var st = SmartText.CreateFrom(conf.Message);
|
||||
foreach (var modelRep in model.GetReplacements())
|
||||
{
|
||||
rctx.WithOverride(modelRep.Key, () => modelRep.Value(guild));
|
||||
}
|
||||
|
||||
st = await _repSvc.ReplaceAsync(st, rctx);
|
||||
if (st is SmartPlainText spt)
|
||||
{
|
||||
await _mss.Response(channel)
|
||||
.Confirm(spt.Text)
|
||||
.SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _mss.Response(channel)
|
||||
.Text(st)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
public async Task EnableAsync(
|
||||
ulong guildId,
|
||||
ulong channelId,
|
||||
NotifyType nType,
|
||||
string message)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<Notify>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
ChannelId = channelId,
|
||||
Type = nType,
|
||||
Message = message,
|
||||
},
|
||||
(_) => new()
|
||||
{
|
||||
Message = message,
|
||||
ChannelId = channelId
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Type = nType
|
||||
});
|
||||
|
||||
var eventDict = _events.GetOrAdd(nType, _ => new());
|
||||
eventDict[guildId] = new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
ChannelId = channelId,
|
||||
Type = nType,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DisableAsync(ulong guildId, NotifyType nType)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var deleted = await uow.GetTable<Notify>()
|
||||
.Where(x => x.GuildId == guildId && x.Type == nType)
|
||||
.DeleteAsync();
|
||||
|
||||
if (deleted == 0)
|
||||
return;
|
||||
|
||||
if (!_events.TryGetValue(nType, out var guildsDict))
|
||||
return;
|
||||
|
||||
guildsDict.TryRemove(guildId, out _);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<Notify>> GetForGuildAsync(ulong guildId, int page = 0)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(page);
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var list = await ctx.GetTable<Notify>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderBy(x => x.Type)
|
||||
.Skip(page * 10)
|
||||
.Take(10)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<Notify?> GetNotifyAsync(ulong guildId, NotifyType nType)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<Notify>()
|
||||
.Where(x => x.GuildId == guildId && x.Type == nType)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ namespace NadekoBot.Modules.Administration;
|
||||
public partial class Administration
|
||||
{
|
||||
[Group]
|
||||
public partial class PlayingRotateCommands : NadekoModule<PlayingRotateService>
|
||||
public partial class PlayingRotateCommands : NadekoModule<IBotActivityService>
|
||||
{
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
|
@@ -28,17 +28,17 @@ public partial class Administration
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task AntiAlt(
|
||||
StoopidTime minAge,
|
||||
ParsedTimespan minAge,
|
||||
PunishmentAction action,
|
||||
[Leftover] StoopidTime punishTime = null)
|
||||
[Leftover] ParsedTimespan punishTimespan = null)
|
||||
{
|
||||
var minAgeMinutes = (int)minAge.Time.TotalMinutes;
|
||||
var punishTimeMinutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
|
||||
var punishTimeMinutes = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
|
||||
|
||||
if (minAgeMinutes < 1 || punishTimeMinutes < 0)
|
||||
return;
|
||||
|
||||
var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
|
||||
var minutes = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
|
||||
if (action is PunishmentAction.TimeOut && minutes < 1)
|
||||
minutes = 1;
|
||||
|
||||
@@ -53,7 +53,7 @@ public partial class Administration
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] IRole role)
|
||||
public async Task AntiAlt(ParsedTimespan minAge, PunishmentAction action, [Leftover] IRole role)
|
||||
{
|
||||
var minAgeMinutes = (int)minAge.Time.TotalMinutes;
|
||||
|
||||
@@ -86,8 +86,8 @@ public partial class Administration
|
||||
int userThreshold,
|
||||
int seconds,
|
||||
PunishmentAction action,
|
||||
[Leftover] StoopidTime punishTime)
|
||||
=> InternalAntiRaid(userThreshold, seconds, action, punishTime);
|
||||
[Leftover] ParsedTimespan punishTimespan)
|
||||
=> InternalAntiRaid(userThreshold, seconds, action, punishTimespan);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
@@ -100,7 +100,7 @@ public partial class Administration
|
||||
int userThreshold,
|
||||
int seconds = 10,
|
||||
PunishmentAction action = PunishmentAction.Mute,
|
||||
StoopidTime punishTime = null)
|
||||
ParsedTimespan punishTimespan = null)
|
||||
{
|
||||
if (action == PunishmentAction.AddRole)
|
||||
{
|
||||
@@ -120,13 +120,13 @@ public partial class Administration
|
||||
return;
|
||||
}
|
||||
|
||||
if (punishTime is not null)
|
||||
if (punishTimespan is not null)
|
||||
{
|
||||
if (!_service.IsDurationAllowed(action))
|
||||
await Response().Error(strs.prot_cant_use_time).SendAsync();
|
||||
}
|
||||
|
||||
var time = (int?)punishTime?.Time.TotalMinutes ?? 0;
|
||||
var time = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
|
||||
if (time is < 0 or > 60 * 24)
|
||||
return;
|
||||
|
||||
@@ -170,8 +170,8 @@ public partial class Administration
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[Priority(1)]
|
||||
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime)
|
||||
=> InternalAntiSpam(messageCount, action, punishTime);
|
||||
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] ParsedTimespan punishTimespan)
|
||||
=> InternalAntiSpam(messageCount, action, punishTimespan);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
@@ -183,19 +183,19 @@ public partial class Administration
|
||||
private async Task InternalAntiSpam(
|
||||
int messageCount,
|
||||
PunishmentAction action,
|
||||
StoopidTime timeData = null,
|
||||
ParsedTimespan timespanData = null,
|
||||
IRole role = null)
|
||||
{
|
||||
if (messageCount is < 2 or > 10)
|
||||
return;
|
||||
|
||||
if (timeData is not null)
|
||||
if (timespanData is not null)
|
||||
{
|
||||
if (!_service.IsDurationAllowed(action))
|
||||
await Response().Error(strs.prot_cant_use_time).SendAsync();
|
||||
}
|
||||
|
||||
var time = (int?)timeData?.Time.TotalMinutes ?? 0;
|
||||
var time = (int?)timespanData?.Time.TotalMinutes ?? 0;
|
||||
if (time is < 0 or > 60 * 24)
|
||||
return;
|
||||
|
||||
|
@@ -22,6 +22,7 @@ public class ProtectionService : INService
|
||||
private readonly MuteService _mute;
|
||||
private readonly DbService _db;
|
||||
private readonly UserPunishService _punishService;
|
||||
private readonly INotifySubscriber _notifySub;
|
||||
|
||||
private readonly Channel<PunishQueueItem> _punishUserQueue =
|
||||
Channel.CreateUnbounded<PunishQueueItem>(new()
|
||||
@@ -35,12 +36,14 @@ public class ProtectionService : INService
|
||||
IBot bot,
|
||||
MuteService mute,
|
||||
DbService db,
|
||||
UserPunishService punishService)
|
||||
UserPunishService punishService,
|
||||
INotifySubscriber notifySub)
|
||||
{
|
||||
_client = client;
|
||||
_mute = mute;
|
||||
_db = db;
|
||||
_punishService = punishService;
|
||||
_notifySub = notifySub;
|
||||
|
||||
var ids = client.GetGuildIds();
|
||||
using (var uow = db.GetDbContext())
|
||||
@@ -175,6 +178,9 @@ public class ProtectionService : INService
|
||||
alts.RoleId,
|
||||
user);
|
||||
|
||||
await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
|
||||
ProtectionType.Alting,
|
||||
user.Id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -194,6 +200,8 @@ public class ProtectionService : INService
|
||||
var settings = stats.AntiRaidSettings;
|
||||
|
||||
await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users);
|
||||
await _notifySub.NotifyAsync(
|
||||
new ProtectionNotifyModel(user.Guild.Id, ProtectionType.Raiding, users[0].Id));
|
||||
}
|
||||
|
||||
await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
|
||||
@@ -246,6 +254,10 @@ public class ProtectionService : INService
|
||||
settings.MuteTime,
|
||||
settings.RoleId,
|
||||
(IGuildUser)msg.Author);
|
||||
|
||||
await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
|
||||
ProtectionType.Spamming,
|
||||
msg.Author.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
#nullable disable
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using NadekoBot.Common.TypeReaders.Models;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
@@ -13,13 +15,18 @@ public partial class Administration
|
||||
Excl
|
||||
}
|
||||
|
||||
private readonly TempRoleService _tempRoleService;
|
||||
private readonly IServiceProvider _services;
|
||||
private StickyRolesService _stickyRoleSvc;
|
||||
|
||||
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc)
|
||||
public RoleCommands(
|
||||
IServiceProvider services,
|
||||
StickyRolesService stickyRoleSvc,
|
||||
TempRoleService tempRoleService)
|
||||
{
|
||||
_services = services;
|
||||
_stickyRoleSvc = stickyRoleSvc;
|
||||
_tempRoleService = tempRoleService;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
@@ -34,13 +41,16 @@ public partial class Administration
|
||||
return;
|
||||
try
|
||||
{
|
||||
await targetUser.AddRoleAsync(roleToAdd, new RequestOptions()
|
||||
{
|
||||
AuditLogReason = $"Added by [{ctx.User.Username}]"
|
||||
});
|
||||
await targetUser.AddRoleAsync(roleToAdd,
|
||||
new RequestOptions()
|
||||
{
|
||||
AuditLogReason = $"Added by [{ctx.User.Username}]"
|
||||
});
|
||||
|
||||
await Response().Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
|
||||
Format.Bold(targetUser.ToString()))).SendAsync();
|
||||
await Response()
|
||||
.Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
|
||||
Format.Bold(targetUser.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -62,8 +72,10 @@ public partial class Administration
|
||||
try
|
||||
{
|
||||
await targetUser.RemoveRoleAsync(roleToRemove);
|
||||
await Response().Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
|
||||
Format.Bold(targetUser.ToString()))).SendAsync();
|
||||
await Response()
|
||||
.Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
|
||||
Format.Bold(targetUser.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -204,5 +216,29 @@ public partial class Administration
|
||||
await Response().Confirm(strs.sticky_roles_disabled).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
public async Task TempRole(ParsedTimespan timespan, IUser user, [Leftover] IRole role)
|
||||
{
|
||||
if (!await CheckRoleHierarchy(role))
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.hierarchy)
|
||||
.SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _tempRoleService.AddTempRoleAsync(ctx.Guild.Id, role.Id, user.Id, timespan.Time);
|
||||
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.temp_role_added(user.Mention,
|
||||
Format.Bold(role.Name),
|
||||
TimestampTag.FromDateTime(DateTime.UtcNow.Add(timespan.Time), TimestampTagStyles.Relative)))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
140
src/NadekoBot/Modules/Administration/Role/TempRoleService.cs
Normal file
140
src/NadekoBot/Modules/Administration/Role/TempRoleService.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public class TempRoleService : IReadyExecutor, INService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCreds _creds;
|
||||
|
||||
private TaskCompletionSource<bool> _tcs = new();
|
||||
|
||||
public TempRoleService(
|
||||
DbService db,
|
||||
DiscordSocketClient client,
|
||||
IBotCreds creds)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public async Task AddTempRoleAsync(
|
||||
ulong guildId,
|
||||
ulong roleId,
|
||||
ulong userId,
|
||||
TimeSpan duration)
|
||||
{
|
||||
if (duration == TimeSpan.Zero)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<TempRole>()
|
||||
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||
.DeleteAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var until = DateTime.UtcNow.Add(duration);
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.GetTable<TempRole>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
RoleId = roleId,
|
||||
UserId = userId,
|
||||
Remove = false,
|
||||
ExpiresAt = until
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
ExpiresAt = until,
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
UserId = userId,
|
||||
RoleId = roleId
|
||||
});
|
||||
|
||||
_tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var latest = await _db.GetDbContext()
|
||||
.GetTable<TempRole>()
|
||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
|
||||
_creds.TotalShards,
|
||||
_client.ShardId))
|
||||
.OrderBy(x => x.ExpiresAt)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
|
||||
if (latest == default)
|
||||
{
|
||||
await _tcs.Task;
|
||||
continue;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (latest.ExpiresAt > now)
|
||||
{
|
||||
await Task.WhenAny(Task.Delay(latest.ExpiresAt - now), _tcs.Task);
|
||||
continue;
|
||||
}
|
||||
|
||||
var deleted = await _db.GetDbContext()
|
||||
.GetTable<TempRole>()
|
||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
|
||||
_creds.TotalShards,
|
||||
_client.ShardId)
|
||||
&& x.ExpiresAt <= now)
|
||||
.DeleteWithOutputAsync();
|
||||
|
||||
foreach (var d in deleted)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RemoveRole(d);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Warning("Unable to remove temp role {RoleId} from user {UserId}",
|
||||
d.RoleId,
|
||||
d.UserId);
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Unexpected error occurred in temprole loop");
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveRole(TempRole tempRole)
|
||||
{
|
||||
var guild = _client.GetGuild(tempRole.GuildId);
|
||||
|
||||
var role = guild?.GetRole(tempRole.RoleId);
|
||||
if (role is null)
|
||||
return;
|
||||
|
||||
var user = guild?.GetUser(tempRole.UserId);
|
||||
if (user is null)
|
||||
return;
|
||||
|
||||
await user.RemoveRoleAsync(role);
|
||||
}
|
||||
}
|
@@ -1,71 +1,32 @@
|
||||
#nullable disable
|
||||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Services;
|
||||
|
||||
public sealed class PlayingRotateService : INService, IReadyExecutor
|
||||
public sealed class BotActivityService : IBotActivityService, IReadyExecutor, INService
|
||||
{
|
||||
private readonly BotConfigService _bss;
|
||||
private readonly SelfService _selfService;
|
||||
private readonly TypedKey<ActivityPubData> _activitySetKey = new("activity.set");
|
||||
|
||||
private readonly IReplacementService _repService;
|
||||
|
||||
// private readonly Replacer _rep;
|
||||
private readonly DbService _db;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly DbService _db;
|
||||
private readonly IReplacementService _rep;
|
||||
private readonly BotConfigService _bss;
|
||||
|
||||
public PlayingRotateService(
|
||||
public BotActivityService(
|
||||
IPubSub pubSub,
|
||||
DiscordSocketClient client,
|
||||
DbService db,
|
||||
BotConfigService bss,
|
||||
IEnumerable<IPlaceholderProvider> phProviders,
|
||||
SelfService selfService,
|
||||
IReplacementService repService)
|
||||
IReplacementService rep,
|
||||
BotConfigService bss)
|
||||
{
|
||||
_db = db;
|
||||
_bss = bss;
|
||||
_selfService = selfService;
|
||||
_repService = repService;
|
||||
_pubSub = pubSub;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
if (_client.ShardId != 0)
|
||||
return;
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
|
||||
var index = 0;
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_bss.Data.RotateStatuses)
|
||||
continue;
|
||||
|
||||
IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
rotatingStatuses = uow.Set<RotatingPlayingStatus>().AsNoTracking().OrderBy(x => x.Id).ToList();
|
||||
}
|
||||
|
||||
if (rotatingStatuses.Count == 0)
|
||||
continue;
|
||||
|
||||
var playingStatus = index >= rotatingStatuses.Count
|
||||
? rotatingStatuses[index = 0]
|
||||
: rotatingStatuses[index++];
|
||||
|
||||
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new(client: _client));
|
||||
await _selfService.SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
_db = db;
|
||||
_rep = rep;
|
||||
_bss = bss;
|
||||
}
|
||||
|
||||
public async Task<string> RemovePlayingAsync(int index)
|
||||
@@ -116,4 +77,91 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
|
||||
using var uow = _db.GetDbContext();
|
||||
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList();
|
||||
}
|
||||
|
||||
public Task SetActivityAsync(string game, ActivityType? type)
|
||||
=> _pubSub.Pub(_activitySetKey,
|
||||
new()
|
||||
{
|
||||
Name = game,
|
||||
Link = null,
|
||||
Type = type
|
||||
});
|
||||
|
||||
public Task SetStreamAsync(string name, string link)
|
||||
=> _pubSub.Pub(_activitySetKey,
|
||||
new()
|
||||
{
|
||||
Name = name,
|
||||
Link = link,
|
||||
Type = ActivityType.Streaming
|
||||
});
|
||||
|
||||
private sealed class ActivityPubData
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public string Link { get; init; }
|
||||
public ActivityType? Type { get; init; }
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
await _pubSub.Sub(_activitySetKey,
|
||||
async data =>
|
||||
{
|
||||
if (_client.ShardId == 0)
|
||||
{
|
||||
DisableRotatePlaying();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (data.Type is { } activityType)
|
||||
{
|
||||
await _client.SetGameAsync(data.Name, data.Link, activityType);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _client.SetCustomStatusAsync(data.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error setting activity");
|
||||
}
|
||||
});
|
||||
|
||||
if (_client.ShardId != 0)
|
||||
return;
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
|
||||
var index = 0;
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_bss.Data.RotateStatuses)
|
||||
continue;
|
||||
|
||||
IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
rotatingStatuses = uow.Set<RotatingPlayingStatus>().AsNoTracking().OrderBy(x => x.Id).ToList();
|
||||
}
|
||||
|
||||
if (rotatingStatuses.Count == 0)
|
||||
continue;
|
||||
|
||||
var playingStatus = index >= rotatingStatuses.Count
|
||||
? rotatingStatuses[index = 0]
|
||||
: rotatingStatuses[index++];
|
||||
|
||||
var statusText = await _rep.ReplaceAsync(playingStatus.Status, new(client: _client));
|
||||
await SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Services;
|
||||
|
||||
public interface IBotActivityService
|
||||
{
|
||||
Task SetActivityAsync(string game, ActivityType? type);
|
||||
Task SetStreamAsync(string name, string link);
|
||||
bool ToggleRotatePlaying();
|
||||
Task AddPlaying(ActivityType statusType, string status);
|
||||
Task<string> RemovePlayingAsync(int index);
|
||||
IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses();
|
||||
}
|
@@ -24,19 +24,22 @@ public partial class Administration
|
||||
private readonly IMedusaLoaderService _medusaLoader;
|
||||
private readonly ICoordinator _coord;
|
||||
private readonly DbService _db;
|
||||
private readonly IBotActivityService _bas;
|
||||
|
||||
public SelfCommands(
|
||||
DiscordSocketClient client,
|
||||
DbService db,
|
||||
IBotStrings strings,
|
||||
ICoordinator coord,
|
||||
IMedusaLoaderService medusaLoader)
|
||||
IMedusaLoaderService medusaLoader,
|
||||
IBotActivityService bas)
|
||||
{
|
||||
_client = client;
|
||||
_db = db;
|
||||
_strings = strings;
|
||||
_coord = coord;
|
||||
_medusaLoader = medusaLoader;
|
||||
_bas = bas;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +66,9 @@ public partial class Administration
|
||||
|
||||
await message.ModifyAsync(x =>
|
||||
x.Embed = CreateEmbed()
|
||||
.WithDescription(GetText(strs.cache_users_done(added, updated)))
|
||||
.WithOkColor()
|
||||
.Build()
|
||||
.WithDescription(GetText(strs.cache_users_done(added, updated)))
|
||||
.WithOkColor()
|
||||
.Build()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,13 +119,13 @@ public partial class Administration
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.scadd))
|
||||
.AddField(GetText(strs.server),
|
||||
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
|
||||
true)
|
||||
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
|
||||
.AddField(GetText(strs.command_text), cmdText))
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.scadd))
|
||||
.AddField(GetText(strs.server),
|
||||
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
|
||||
true)
|
||||
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
|
||||
.AddField(GetText(strs.command_text), cmdText))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
@@ -496,7 +499,7 @@ public partial class Administration
|
||||
// var rep = new ReplacementBuilder().WithDefault(Context).Build();
|
||||
|
||||
var repCtx = new ReplacementContext(ctx);
|
||||
await _service.SetActivityAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type);
|
||||
await _bas.SetActivityAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type);
|
||||
|
||||
await Response().Confirm(strs.set_activity).SendAsync();
|
||||
}
|
||||
@@ -518,7 +521,7 @@ public partial class Administration
|
||||
{
|
||||
name ??= "";
|
||||
|
||||
await _service.SetStreamAsync(name, url);
|
||||
await _bas.SetStreamAsync(name, url);
|
||||
|
||||
await Response().Confirm(strs.set_stream).SendAsync();
|
||||
}
|
||||
|
@@ -28,7 +28,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
||||
private readonly IMessageSenderService _sender;
|
||||
|
||||
//keys
|
||||
private readonly TypedKey<ActivityPubData> _activitySetKey;
|
||||
private readonly TypedKey<string> _guildLeaveKey;
|
||||
|
||||
public SelfService(
|
||||
@@ -51,11 +50,8 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
||||
_bss = bss;
|
||||
_pubSub = pubSub;
|
||||
_sender = sender;
|
||||
_activitySetKey = new("activity.set");
|
||||
_guildLeaveKey = new("guild.leave");
|
||||
|
||||
HandleStatusChanges();
|
||||
|
||||
_pubSub.Sub(_guildLeaveKey,
|
||||
async input =>
|
||||
{
|
||||
@@ -394,49 +390,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
||||
return channelId is not null;
|
||||
}
|
||||
|
||||
private void HandleStatusChanges()
|
||||
=> _pubSub.Sub(_activitySetKey,
|
||||
async data =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (data.Type is { } activityType)
|
||||
await _client.SetGameAsync(data.Name, data.Link, activityType);
|
||||
else
|
||||
await _client.SetCustomStatusAsync(data.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error setting activity");
|
||||
}
|
||||
});
|
||||
|
||||
public Task SetActivityAsync(string game, ActivityType? type)
|
||||
=> _pubSub.Pub(_activitySetKey,
|
||||
new()
|
||||
{
|
||||
Name = game,
|
||||
Link = null,
|
||||
Type = type
|
||||
});
|
||||
|
||||
public Task SetStreamAsync(string name, string link)
|
||||
=> _pubSub.Pub(_activitySetKey,
|
||||
new()
|
||||
{
|
||||
Name = name,
|
||||
Link = link,
|
||||
Type = ActivityType.Streaming
|
||||
});
|
||||
|
||||
private sealed class ActivityPubData
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public string Link { get; init; }
|
||||
public ActivityType? Type { get; init; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name
|
||||
/// and discriminator is present in <paramref name="users"/>, their name and discriminator get updated accordingly.
|
||||
|
@@ -84,7 +84,7 @@ public partial class Administration
|
||||
IUserMessage msg = null;
|
||||
try
|
||||
{
|
||||
if (guildUser.RoleIds.Contains(role.Id))
|
||||
if (!guildUser.RoleIds.Contains(role.Id))
|
||||
{
|
||||
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
|
||||
return;
|
||||
|
@@ -313,7 +313,7 @@ public partial class Administration
|
||||
int number,
|
||||
AddRole _,
|
||||
IRole role,
|
||||
StoopidTime time = null)
|
||||
ParsedTimespan timespan = null)
|
||||
{
|
||||
var punish = PunishmentAction.AddRole;
|
||||
|
||||
@@ -324,12 +324,12 @@ public partial class Administration
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
|
||||
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, timespan, role);
|
||||
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
if (time is null)
|
||||
if (timespan is null)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||
@@ -341,7 +341,7 @@ public partial class Administration
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString()),
|
||||
Format.Bold(time.Input)))
|
||||
Format.Bold(timespan.Input)))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
@@ -349,7 +349,7 @@ public partial class Administration
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null)
|
||||
public async Task WarnPunish(int number, PunishmentAction punish, ParsedTimespan timespan = null)
|
||||
{
|
||||
// this should never happen. Addrole has its own method with higher priority
|
||||
// also disallow warn punishment for getting warned
|
||||
@@ -357,15 +357,15 @@ public partial class Administration
|
||||
return;
|
||||
|
||||
// you must specify the time for timeout
|
||||
if (punish is PunishmentAction.TimeOut && time is null)
|
||||
if (punish is PunishmentAction.TimeOut && timespan is null)
|
||||
return;
|
||||
|
||||
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time);
|
||||
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, timespan);
|
||||
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
if (time is null)
|
||||
if (timespan is null)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
|
||||
@@ -377,7 +377,7 @@ public partial class Administration
|
||||
await Response()
|
||||
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
|
||||
Format.Bold(number.ToString()),
|
||||
Format.Bold(time.Input)))
|
||||
Format.Bold(timespan.Input)))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
@@ -417,17 +417,17 @@ public partial class Administration
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
[BotPerm(GuildPerm.BanMembers)]
|
||||
[Priority(1)]
|
||||
public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
|
||||
=> Ban(time, user.Id, msg);
|
||||
public Task Ban(ParsedTimespan timespan, IUser user, [Leftover] string msg = null)
|
||||
=> Ban(timespan, user.Id, msg);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
[BotPerm(GuildPerm.BanMembers)]
|
||||
[Priority(0)]
|
||||
public async Task Ban(StoopidTime time, ulong userId, [Leftover] string msg = null)
|
||||
public async Task Ban(ParsedTimespan timespan, ulong userId, [Leftover] string msg = null)
|
||||
{
|
||||
if (time.Time > TimeSpan.FromDays(49))
|
||||
if (timespan.Time > TimeSpan.FromDays(49))
|
||||
return;
|
||||
|
||||
var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||
@@ -444,7 +444,7 @@ public partial class Administration
|
||||
{
|
||||
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
|
||||
var smartText =
|
||||
await _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
|
||||
await _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, timespan.Time);
|
||||
if (smartText is not null)
|
||||
await Response().User(guildUser).Text(smartText).SendAsync();
|
||||
}
|
||||
@@ -456,14 +456,14 @@ public partial class Administration
|
||||
|
||||
var user = await ctx.Client.GetUserAsync(userId);
|
||||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
|
||||
await _mute.TimedBan(ctx.Guild, userId, timespan.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
|
||||
.AddField("ID", userId.ToString(), true)
|
||||
.AddField(GetText(strs.duration),
|
||||
time.Time.ToPrettyStringHm(),
|
||||
timespan.Time.ToPrettyStringHm(),
|
||||
true);
|
||||
|
||||
if (dmFailed)
|
||||
@@ -601,7 +601,7 @@ public partial class Administration
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
[BotPerm(GuildPerm.BanMembers)]
|
||||
[Priority(1)]
|
||||
public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null)
|
||||
public Task BanMessageTest(ParsedTimespan duration, [Leftover] string reason = null)
|
||||
=> InternalBanMessageTest(reason, duration.Time);
|
||||
|
||||
private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
|
||||
@@ -790,7 +790,7 @@ public partial class Administration
|
||||
[UserPerm(GuildPerm.ModerateMembers)]
|
||||
[BotPerm(GuildPerm.ModerateMembers)]
|
||||
[Priority(2)]
|
||||
public async Task Timeout(IUser globalUser, StoopidTime time, [Leftover] string msg = null)
|
||||
public async Task Timeout(IUser globalUser, ParsedTimespan timespan, [Leftover] string msg = null)
|
||||
{
|
||||
var user = await ctx.Guild.GetUserAsync(globalUser.Id);
|
||||
|
||||
@@ -816,7 +816,7 @@ public partial class Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await user.SetTimeOutAsync(time.Time);
|
||||
await user.SetTimeOutAsync(timespan.Time);
|
||||
|
||||
var toSend = CreateEmbed()
|
||||
.WithOkColor()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Modules.Xp.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
@@ -10,13 +11,19 @@ public partial class Gambling
|
||||
public sealed class BetStatsCommands : GamblingModule<UserBetStatsService>
|
||||
{
|
||||
private readonly GamblingTxTracker _gamblingTxTracker;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public BetStatsCommands(
|
||||
GamblingTxTracker gamblingTxTracker,
|
||||
GamblingConfigService gcs)
|
||||
GamblingConfigService gcs,
|
||||
IBotCache cache,
|
||||
IUserService userService)
|
||||
: base(gcs)
|
||||
{
|
||||
_gamblingTxTracker = gamblingTxTracker;
|
||||
_cache = cache;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
@@ -25,12 +32,12 @@ public partial class Gambling
|
||||
var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game);
|
||||
|
||||
var result = await PromptUserConfirmAsync(CreateEmbed()
|
||||
.WithDescription(
|
||||
$"""
|
||||
Are you sure you want to reset your bet stats for **{GetGameName(game)}**?
|
||||
.WithDescription(
|
||||
$"""
|
||||
Are you sure you want to reset your bet stats for **{GetGameName(game)}**?
|
||||
|
||||
It will cost you {N(price)}
|
||||
"""));
|
||||
It will cost you {N(price)}
|
||||
"""));
|
||||
|
||||
if (!result)
|
||||
return;
|
||||
@@ -88,15 +95,15 @@ public partial class Gambling
|
||||
};
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(user)
|
||||
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
|
||||
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
|
||||
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
|
||||
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
|
||||
.AddField("Payout",
|
||||
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
|
||||
true);
|
||||
.WithOkColor()
|
||||
.WithAuthor(user)
|
||||
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
|
||||
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
|
||||
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
|
||||
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
|
||||
.AddField("Payout",
|
||||
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
|
||||
true);
|
||||
if (game == null)
|
||||
{
|
||||
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
|
||||
@@ -115,13 +122,86 @@ public partial class Gambling
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private readonly record struct WinLbStat(
|
||||
int Rank,
|
||||
string User,
|
||||
GamblingGame Game,
|
||||
long MaxWin);
|
||||
|
||||
private TypedKey<List<WinLbStat>> GetWinLbKey(int page)
|
||||
=> new($"winlb:{page}");
|
||||
|
||||
private async Task<IReadOnlyCollection<WinLbStat>> GetCachedWinLbAsync(int page)
|
||||
{
|
||||
return await _cache.GetOrAddAsync(GetWinLbKey(page),
|
||||
async () =>
|
||||
{
|
||||
var items = await _service.GetWinLbAsync(page);
|
||||
|
||||
if (items.Count == 0)
|
||||
return [];
|
||||
|
||||
var outputItems = new List<WinLbStat>(items.Count);
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var x = items[i];
|
||||
var user = (await ctx.Client.GetUserAsync(x.UserId, CacheMode.CacheOnly))?.ToString()
|
||||
?? (await _userService.GetUserAsync(x.UserId))?.Username
|
||||
?? x.UserId.ToString();
|
||||
|
||||
if (user.StartsWith("??"))
|
||||
user = x.UserId.ToString();
|
||||
|
||||
outputItems.Add(new WinLbStat(i + 1 + (page * 9), user, x.Game, x.MaxWin));
|
||||
}
|
||||
|
||||
return outputItems;
|
||||
},
|
||||
expiry: TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task WinLb(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(p => GetCachedWinLbAsync(p))
|
||||
.PageSize(9)
|
||||
.Page((items, curPage) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.winlb))
|
||||
.WithOkColor();
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
eb.WithDescription(GetText(strs.empty_page));
|
||||
return eb;
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
eb.AddField($"#{item.Rank} {item.User}",
|
||||
$"{N(item.MaxWin)}\n`{item.Game.ToString().ToLower()}`",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task GambleStats()
|
||||
{
|
||||
var stats = await _gamblingTxTracker.GetAllAsync();
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor();
|
||||
.WithOkColor();
|
||||
|
||||
var str = "` Feature `|` Bet `|`Paid Out`|` RoI `\n";
|
||||
str += "――――――――――――――――――――\n";
|
||||
@@ -157,13 +237,13 @@ public partial class Gambling
|
||||
public async Task GambleStatsReset()
|
||||
{
|
||||
if (!await PromptUserConfirmAsync(CreateEmbed()
|
||||
.WithDescription(
|
||||
"""
|
||||
Are you sure?
|
||||
This will completely reset Gambling Stats.
|
||||
.WithDescription(
|
||||
"""
|
||||
Are you sure?
|
||||
This will completely reset Gambling Stats.
|
||||
|
||||
This action is irreversible.
|
||||
""")))
|
||||
This action is irreversible.
|
||||
""")))
|
||||
return;
|
||||
|
||||
await GambleStats();
|
||||
|
@@ -135,7 +135,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
});
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Timely()
|
||||
{
|
||||
var val = Config.Timely.Amount;
|
||||
|
@@ -42,7 +42,7 @@ public sealed class UserBetStatsService : INService
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.GetTable<UserBetStats>()
|
||||
.DeleteAsync(x => x.UserId == userId && (game == null || x.Game == game));
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,4 +52,16 @@ public sealed class UserBetStatsService : INService
|
||||
await ctx.GetTable<GamblingStats>()
|
||||
.DeleteAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<UserBetStats>> GetWinLbAsync(int page)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(page);
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<UserBetStats>()
|
||||
.OrderByDescending(x => x.MaxWin)
|
||||
.Skip(page * 9)
|
||||
.Take(9)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using System.Text;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
@@ -38,10 +39,72 @@ public partial class Games : NadekoModule<GamesService>
|
||||
return;
|
||||
|
||||
var res = _service.GetEightballResponse(ctx.User.Id, question);
|
||||
await Response().Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(ctx.User.ToString())
|
||||
.AddField("❓ " + GetText(strs.question), question)
|
||||
.AddField("🎱 " + GetText(strs._8ball), res)).SendAsync();
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(ctx.User.ToString())
|
||||
.AddField("❓ " + GetText(strs.question), question)
|
||||
.AddField("🎱 " + GetText(strs._8ball), res))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private readonly string[] _numberEmojis = ["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"];
|
||||
|
||||
[Cmd]
|
||||
public async Task Minesweeper(int numberOfMines = 12)
|
||||
{
|
||||
var boardSizeX = 9;
|
||||
var boardSizeY = 10;
|
||||
|
||||
if (numberOfMines < 1)
|
||||
{
|
||||
numberOfMines = 1;
|
||||
}
|
||||
else if (numberOfMines > boardSizeX * boardSizeY / 2)
|
||||
{
|
||||
numberOfMines = boardSizeX * boardSizeY / 2;
|
||||
}
|
||||
|
||||
var mineIndicies = Enumerable.Range(0, boardSizeX * boardSizeY)
|
||||
.ToArray()
|
||||
.Shuffle()
|
||||
.Take(numberOfMines)
|
||||
.ToHashSet();
|
||||
|
||||
string GetNumberOnCell(int x, int y)
|
||||
{
|
||||
var count = 0;
|
||||
for (var i = -1; i < 2; i++)
|
||||
{
|
||||
for (var j = -1; j < 2; j++)
|
||||
{
|
||||
if (y + j >= boardSizeY || y + j < 0)
|
||||
continue;
|
||||
if (x + i >= boardSizeX || x + i < 0)
|
||||
continue;
|
||||
|
||||
var boardIndex = (y + j) * boardSizeX + (x + i);
|
||||
if (mineIndicies.Contains(boardIndex))
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return _numberEmojis[count];
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"### Minesweeper [{numberOfMines}\\💣]");
|
||||
for (var i = 0; i < boardSizeY; i++)
|
||||
{
|
||||
for (var j = 0; j < boardSizeX; j++)
|
||||
{
|
||||
var emoji = mineIndicies.Contains((i * boardSizeX) + j) ? "💣" : GetNumberOnCell(j, i);
|
||||
sb.Append($"||{emoji}||");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
await Response().Text(sb.ToString()).SendAsync();
|
||||
}
|
||||
}
|
@@ -65,7 +65,17 @@ public sealed class MusicPlayer : IMusicPlayer
|
||||
|
||||
_songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
|
||||
|
||||
_thread = new(async () => { await PlayLoop(); });
|
||||
_thread = new(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await PlayLoop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Music player thread crashed");
|
||||
}
|
||||
});
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
@@ -402,12 +412,24 @@ public sealed class MusicPlayer : IMusicPlayer
|
||||
if (song is null)
|
||||
return default;
|
||||
|
||||
int index;
|
||||
|
||||
if (asNext)
|
||||
return (_queue.EnqueueNext(song, queuer, out index), index);
|
||||
var wasLast = _queue.IsLast();
|
||||
|
||||
return (_queue.Enqueue(song, queuer, out index), index);
|
||||
try
|
||||
{
|
||||
int index;
|
||||
if (asNext)
|
||||
return (_queue.EnqueueNext(song, queuer, out index), index);
|
||||
|
||||
return (_queue.Enqueue(song, queuer, out index), index);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// if (wasLast && IsStopped)
|
||||
// {
|
||||
// IsStopped = false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)
|
||||
|
@@ -73,5 +73,38 @@ public partial class Permissions
|
||||
|
||||
await Response().Confirm(strs.gcmd_remove(Format.Bold(cmd.Name))).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task DmModule(ModuleOrExpr module)
|
||||
{
|
||||
var moduleName = module.Name.ToLowerInvariant();
|
||||
|
||||
var added = _service.ToggleModule(moduleName, true);
|
||||
|
||||
if (added)
|
||||
{
|
||||
await Response().Confirm(strs.dmmod_add(Format.Bold(module.Name))).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response().Confirm(strs.dmmod_remove(Format.Bold(module.Name))).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task DmCommand(CommandOrExprInfo cmd)
|
||||
{
|
||||
var commandName = cmd.Name.ToLowerInvariant();
|
||||
var added = _service.ToggleCommand(commandName, true);
|
||||
|
||||
if (added)
|
||||
{
|
||||
await Response().Confirm(strs.dmcmd_add(Format.Bold(cmd.Name))).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response().Confirm(strs.dmcmd_remove(Format.Bold(cmd.Name))).SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,10 +24,19 @@ public class GlobalPermissionService : IExecPreCommand, INService
|
||||
var settings = _bss.Data;
|
||||
var commandName = command.Name.ToLowerInvariant();
|
||||
|
||||
if (commandName != "resetglobalperms"
|
||||
&& (settings.Blocked.Commands.Contains(commandName)
|
||||
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant())))
|
||||
return Task.FromResult(true);
|
||||
if (commandName != "resetglobalperms")
|
||||
{
|
||||
if (settings.Blocked.Commands.Contains(commandName)
|
||||
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))
|
||||
return Task.FromResult(true);
|
||||
|
||||
if (ctx.Guild is null)
|
||||
{
|
||||
if (settings.DmBlocked.Commands.Contains(commandName)
|
||||
|| settings.DmBlocked.Modules.Contains(moduleName.ToLowerInvariant()))
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
@@ -37,13 +46,30 @@ public class GlobalPermissionService : IExecPreCommand, INService
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Lowercase module name</param>
|
||||
/// <returns>Whether the module is added</returns>
|
||||
public bool ToggleModule(string moduleName)
|
||||
public bool ToggleModule(string moduleName, bool priv = false)
|
||||
{
|
||||
var added = false;
|
||||
_bss.ModifyConfig(bs =>
|
||||
{
|
||||
if (priv)
|
||||
{
|
||||
if (bs.DmBlocked.Modules.Add(moduleName))
|
||||
{
|
||||
added = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bs.DmBlocked.Modules.Remove(moduleName);
|
||||
added = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (bs.Blocked.Modules.Add(moduleName))
|
||||
{
|
||||
added = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bs.Blocked.Modules.Remove(moduleName);
|
||||
@@ -59,13 +85,30 @@ public class GlobalPermissionService : IExecPreCommand, INService
|
||||
/// </summary>
|
||||
/// <param name="commandName">Lowercase command name</param>
|
||||
/// <returns>Whether the command is added</returns>
|
||||
public bool ToggleCommand(string commandName)
|
||||
public bool ToggleCommand(string commandName, bool priv = false)
|
||||
{
|
||||
var added = false;
|
||||
_bss.ModifyConfig(bs =>
|
||||
{
|
||||
if (priv)
|
||||
{
|
||||
if (bs.DmBlocked.Commands.Add(commandName))
|
||||
{
|
||||
added = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bs.DmBlocked.Commands.Remove(commandName);
|
||||
added = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (bs.Blocked.Commands.Add(commandName))
|
||||
{
|
||||
added = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bs.Blocked.Commands.Remove(commandName);
|
||||
|
@@ -103,11 +103,11 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.time_new))
|
||||
.WithDescription(Format.Code(data.Time.ToString(Culture)))
|
||||
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
|
||||
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.time_new))
|
||||
.WithDescription(Format.Code(data.Time.ToString(Culture)))
|
||||
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
|
||||
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
@@ -129,16 +129,16 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(movie.Title)
|
||||
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
|
||||
.WithDescription(movie.Plot.TrimTo(1000))
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
|
||||
? movie.Poster
|
||||
: null))
|
||||
.WithOkColor()
|
||||
.WithTitle(movie.Title)
|
||||
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
|
||||
.WithDescription(movie.Plot.TrimTo(1000))
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
|
||||
? movie.Poster
|
||||
: null))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
@@ -191,9 +191,9 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.original_url), $"<{query}>")
|
||||
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.original_url), $"<{query}>")
|
||||
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
@@ -214,13 +214,13 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(card.Name)
|
||||
.WithDescription(card.Description)
|
||||
.WithImageUrl(card.ImageUrl)
|
||||
.AddField(GetText(strs.store_url), card.StoreUrl, true)
|
||||
.AddField(GetText(strs.cost), card.ManaCost, true)
|
||||
.AddField(GetText(strs.types), card.Types, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(card.Name)
|
||||
.WithDescription(card.Description)
|
||||
.WithImageUrl(card.ImageUrl)
|
||||
.AddField(GetText(strs.store_url), card.StoreUrl, true)
|
||||
.AddField(GetText(strs.cost), card.ManaCost, true)
|
||||
.AddField(GetText(strs.types), card.Types, true);
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
@@ -281,10 +281,10 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
var item = items[0];
|
||||
return CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithUrl(item.Permalink)
|
||||
.WithTitle(item.Word)
|
||||
.WithDescription(item.Definition);
|
||||
.WithOkColor()
|
||||
.WithUrl(item.Permalink)
|
||||
.WithTitle(item.Word)
|
||||
.WithDescription(item.Definition);
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
@@ -312,11 +312,11 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
var model = items.First();
|
||||
var embed = CreateEmbed()
|
||||
.WithDescription(ctx.User.Mention)
|
||||
.AddField(GetText(strs.word), model.Word, true)
|
||||
.AddField(GetText(strs._class), model.WordType, true)
|
||||
.AddField(GetText(strs.definition), model.Definition)
|
||||
.WithOkColor();
|
||||
.WithDescription(ctx.User.Mention)
|
||||
.AddField(GetText(strs.word), model.Word, true)
|
||||
.AddField(GetText(strs._class), model.WordType, true)
|
||||
.AddField(GetText(strs.definition), model.Definition)
|
||||
.WithOkColor();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.Example))
|
||||
embed.AddField(GetText(strs.example), model.Example);
|
||||
@@ -404,10 +404,38 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
await Response()
|
||||
.Embed(
|
||||
CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Username", usr.ToString())
|
||||
.AddField("Avatar Url", avatarUrl)
|
||||
.WithThumbnailUrl(avatarUrl.ToString()))
|
||||
.WithOkColor()
|
||||
.AddField("Username", usr.ToString())
|
||||
.AddField("Avatar Url", avatarUrl)
|
||||
.WithThumbnailUrl(avatarUrl.ToString()))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Banner([Leftover] IGuildUser? usr = null)
|
||||
{
|
||||
usr ??= (IGuildUser)ctx.User;
|
||||
|
||||
var bannerUrl = usr.GetGuildBannerUrl(size: 2048)
|
||||
?? (await ((DiscordSocketClient)ctx.Client).Rest.GetUserAsync(usr.Id))?.GetBannerUrl();
|
||||
|
||||
if (bannerUrl is null)
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.no_banner)
|
||||
.SendAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(
|
||||
CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Username", usr.ToString(), true)
|
||||
.AddField("Banner Url", bannerUrl, true)
|
||||
.WithImageUrl(bannerUrl))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
|
@@ -122,12 +122,15 @@ public sealed partial class FlagTranslateService : IReadyExecutor, INService
|
||||
if (!_supportedFlags.TryGetValue(code, out var lang))
|
||||
return;
|
||||
|
||||
if (!_msgLangs.Add((reaction.MessageId, lang)))
|
||||
if (_msgLangs.Contains((reaction.MessageId, lang)))
|
||||
return;
|
||||
|
||||
var result = await _cache.GetAsync(CdKey(reaction.UserId));
|
||||
if (result.TryPickT0(out _, out _))
|
||||
return;
|
||||
|
||||
if (!_msgLangs.Add((reaction.MessageId, lang)))
|
||||
return;
|
||||
|
||||
await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5));
|
||||
|
||||
|
@@ -28,9 +28,19 @@ public partial class Searches
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
var translation = await _service.Translate(fromLang, toLang, text);
|
||||
|
||||
var embed = CreateEmbed().WithOkColor().AddField(fromLang, text).AddField(toLang, translation);
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(fromLang)
|
||||
.WithDescription(text);
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
var embed2 = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(toLang)
|
||||
.WithDescription(translation);
|
||||
|
||||
await Response()
|
||||
.Embeds([embed, embed2])
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -65,7 +75,10 @@ public partial class Searches
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task AutoTransLang(string fromLang, string toLang)
|
||||
{
|
||||
var succ = await _service.RegisterUserAsync(ctx.User.Id, ctx.Channel.Id, fromLang.ToLower(), toLang.ToLower());
|
||||
var succ = await _service.RegisterUserAsync(ctx.User.Id,
|
||||
ctx.Channel.Id,
|
||||
fromLang.ToLower(),
|
||||
toLang.ToLower());
|
||||
|
||||
if (succ is null)
|
||||
{
|
||||
@@ -87,10 +100,10 @@ public partial class Searches
|
||||
public async Task Translangs()
|
||||
{
|
||||
var langs = _service.GetLanguages().ToList();
|
||||
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.supported_languages))
|
||||
.WithOkColor();
|
||||
.WithTitle(GetText(strs.supported_languages))
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var chunk in langs.Chunk(15))
|
||||
{
|
||||
|
@@ -9,6 +9,7 @@ public sealed class AfkService : INService, IReadyExecutor
|
||||
private readonly MessageSenderService _mss;
|
||||
|
||||
private static readonly TimeSpan _maxAfkDuration = 8.Hours();
|
||||
|
||||
public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss)
|
||||
{
|
||||
_cache = cache;
|
||||
@@ -19,6 +20,9 @@ public sealed class AfkService : INService, IReadyExecutor
|
||||
private static TypedKey<string> GetKey(ulong userId)
|
||||
=> new($"afk:msg:{userId}");
|
||||
|
||||
private static TypedKey<bool> GetRecentlySentKey(ulong userId, ulong channelId)
|
||||
=> new($"afk:recent:{userId}:{channelId}");
|
||||
|
||||
public async Task<bool> SetAfkAsync(ulong userId, string text)
|
||||
{
|
||||
var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true);
|
||||
@@ -43,9 +47,7 @@ public sealed class AfkService : INService, IReadyExecutor
|
||||
msg.DeleteAfter(5);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -61,7 +63,7 @@ public sealed class AfkService : INService, IReadyExecutor
|
||||
await Task.Delay(_maxAfkDuration);
|
||||
_client.MessageReceived -= StopAfk;
|
||||
});
|
||||
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
@@ -72,36 +74,29 @@ public sealed class AfkService : INService, IReadyExecutor
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task TryTriggerAfkMessage(SocketMessage arg)
|
||||
private Task TryTriggerAfkMessage(SocketMessage sm)
|
||||
{
|
||||
if (arg.Author.IsBot || arg.Author.IsWebhook)
|
||||
if (sm.Author.IsBot || sm.Author.IsWebhook)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc)
|
||||
if (sm is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if ((arg.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null)
|
||||
|
||||
if ((sm.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var botUser = await tc.Guild.GetCurrentUserAsync();
|
||||
|
||||
var perms = botUser.GetPermissions(tc);
|
||||
|
||||
if (!perms.SendMessages)
|
||||
return;
|
||||
|
||||
ulong mentionedUserId = 0;
|
||||
|
||||
if (arg.MentionedUsers.Count <= 3)
|
||||
if (sm.MentionedUsers.Count <= 3)
|
||||
{
|
||||
foreach (var uid in uMsg.MentionedUserIds)
|
||||
{
|
||||
if (uid == arg.Author.Id)
|
||||
if (uid == sm.Author.Id)
|
||||
continue;
|
||||
|
||||
if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>"))
|
||||
if (sm.Content.StartsWith($"<@{uid}>") || sm.Content.StartsWith($"<@!{uid}>"))
|
||||
{
|
||||
mentionedUserId = uid;
|
||||
break;
|
||||
@@ -115,26 +110,46 @@ public sealed class AfkService : INService, IReadyExecutor
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
mentionedUserId = repliedUserId;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _cache.GetAsync(GetKey(mentionedUserId));
|
||||
if (result.TryPickT0(out var msg, out _))
|
||||
{
|
||||
var st = SmartText.CreateFrom(msg);
|
||||
|
||||
|
||||
st = $"The user you've pinged (<#{mentionedUserId}>) is AFK: " + st;
|
||||
|
||||
var toDelete = await _mss.Response(arg.Channel)
|
||||
.User(arg.Author)
|
||||
|
||||
var toDelete = await _mss.Response(sm.Channel)
|
||||
.User(sm.Author)
|
||||
.Message(uMsg)
|
||||
.Text(st)
|
||||
.SendAsync();
|
||||
|
||||
toDelete.DeleteAfter(30);
|
||||
|
||||
var botUser = await tc.Guild.GetCurrentUserAsync();
|
||||
var perms = botUser.GetPermissions(tc);
|
||||
if (!perms.SendMessages)
|
||||
return;
|
||||
|
||||
var key = GetRecentlySentKey(mentionedUserId, sm.Channel.Id);
|
||||
var recent = await _cache.GetAsync(key);
|
||||
|
||||
if (!recent.TryPickT0(out _, out _))
|
||||
{
|
||||
var chMsg = await _mss.Response(sm.Channel)
|
||||
.Message(uMsg)
|
||||
.Pending(strs.user_afk($"<@{mentionedUserId}>"))
|
||||
.SendAsync();
|
||||
|
||||
chMsg.DeleteAfter(5);
|
||||
await _cache.AddAsync(key, true, expiry: TimeSpan.FromMinutes(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpException ex)
|
||||
|
@@ -38,6 +38,8 @@ public partial class Utility
|
||||
eb
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.giveaway_started))
|
||||
.AddField(GetText(strs.lasts_until), TimestampTag.FromDateTime(DateTime.UtcNow.Add(duration)), true)
|
||||
// .AddField(GetText(strs.winners_count), "1", true)
|
||||
.WithFooter($"id: {new kwum(id).ToString()}");
|
||||
|
||||
await startingMsg.AddReactionAsync(new Emoji(GiveawayService.GiveawayEmoji));
|
||||
|
@@ -341,6 +341,9 @@ public sealed class GiveawayService : INService, IReadyExecutor
|
||||
try
|
||||
{
|
||||
await msg.ModifyAsync(x => x.Embed = eb.Build());
|
||||
|
||||
if (winner is not null)
|
||||
await _sender.Response(ch).Message(msg).Text($"🎉 <@{winner.UserId}>").SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@@ -12,17 +12,21 @@ public partial class Utility
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ServerColorsShow()
|
||||
{
|
||||
var colors = _service.GetColors(ctx.Guild.Id);
|
||||
var okHex = colors?.Ok?.RawValue.ToString("x6");
|
||||
var warnHex = colors?.Warn?.RawValue.ToString("x6");
|
||||
var errHex = colors?.Error?.RawValue.ToString("x6");
|
||||
EmbedBuilder[] ebs =
|
||||
[
|
||||
CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription("\\✅"),
|
||||
.WithDescription($"\\✅ {okHex}"),
|
||||
CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription("\\⏳\\⚠️"),
|
||||
.WithDescription($"\\⏳\\⚠️ {warnHex}"),
|
||||
CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.WithDescription("\\❌")
|
||||
.WithDescription($"\\❌ {errHex}")
|
||||
];
|
||||
|
||||
await Response()
|
||||
|
@@ -98,10 +98,10 @@ public partial class Utility
|
||||
return;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(guildId is not null
|
||||
? strs.reminder_server_list
|
||||
: strs.reminder_list));
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(guildId is not null
|
||||
? strs.reminder_server_list
|
||||
: strs.reminder_list));
|
||||
|
||||
List<Reminder> rems;
|
||||
if (guildId is { } gid)
|
||||
@@ -193,23 +193,14 @@ public partial class Utility
|
||||
message = message.SanitizeAllMentions();
|
||||
}
|
||||
|
||||
var rem = new Reminder
|
||||
{
|
||||
ChannelId = targetId,
|
||||
IsPrivate = isPrivate,
|
||||
When = time,
|
||||
Message = message,
|
||||
UserId = ctx.User.Id,
|
||||
ServerId = ctx.Guild?.Id ?? 0
|
||||
};
|
||||
await _service.AddReminderAsync(ctx.User.Id,
|
||||
targetId,
|
||||
ctx.Guild?.Id,
|
||||
isPrivate,
|
||||
time,
|
||||
message,
|
||||
ReminderType.User);
|
||||
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.Set<Reminder>().Add(rem);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
|
||||
await Response()
|
||||
.Confirm($"\u23f0 {GetText(strs.remind2(
|
||||
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
|
||||
|
@@ -21,6 +21,8 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
||||
private readonly IMessageSenderService _sender;
|
||||
private readonly CultureInfo _culture;
|
||||
|
||||
private TaskCompletionSource<bool> _tcs;
|
||||
|
||||
public RemindService(
|
||||
DiscordSocketClient client,
|
||||
DbService db,
|
||||
@@ -44,8 +46,7 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
while (true)
|
||||
{
|
||||
await OnReminderLoopTickInternalAsync();
|
||||
}
|
||||
@@ -55,8 +56,7 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var reminders = await GetRemindersBeforeAsync(now);
|
||||
var reminders = await GetRemindersBeforeAsync();
|
||||
if (reminders.Count == 0)
|
||||
return;
|
||||
|
||||
@@ -67,7 +67,6 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
||||
{
|
||||
var executedReminders = group.ToList();
|
||||
await executedReminders.Select(ReminderTimerAction).WhenAll();
|
||||
await RemoveReminders(executedReminders.Select(x => x.Id));
|
||||
await Task.Delay(1500);
|
||||
}
|
||||
}
|
||||
@@ -80,21 +79,51 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
||||
private async Task RemoveReminders(IEnumerable<int> reminders)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.Set<Reminder>()
|
||||
.ToLinqToDBTable()
|
||||
await uow.GetTable<Reminder>()
|
||||
.DeleteAsync(x => reminders.Contains(x.Id));
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
|
||||
private async Task<IReadOnlyList<Reminder>> GetRemindersBeforeAsync()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<Reminder>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId, _creds.TotalShards, _client.ShardId)
|
||||
&& x.When < now)
|
||||
.ToListAsyncLinqToDB();
|
||||
while (true)
|
||||
{
|
||||
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await using var uow = _db.GetDbContext();
|
||||
var earliest = await uow.Set<Reminder>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId,
|
||||
_creds.TotalShards,
|
||||
_client.ShardId))
|
||||
.OrderBy(x => x.When)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
|
||||
if (earliest == default)
|
||||
{
|
||||
await _tcs.Task;
|
||||
continue;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (earliest.When > now)
|
||||
{
|
||||
var diff = earliest.When - now;
|
||||
// Log.Information("Waiting for {Diff}", diff);
|
||||
await Task.WhenAny(Task.Delay(diff), _tcs.Task);
|
||||
continue;
|
||||
}
|
||||
|
||||
var reminders = await uow.Set<Reminder>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId,
|
||||
_creds.TotalShards,
|
||||
_client.ShardId))
|
||||
.Where(x => x.When <= now)
|
||||
.DeleteWithOutputAsync();
|
||||
|
||||
return reminders;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryParseRemindMessage(string input, out RemindObject obj)
|
||||
@@ -243,21 +272,24 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
||||
string message,
|
||||
ReminderType reminderType)
|
||||
{
|
||||
var rem = new Reminder
|
||||
await using (var ctx = _db.GetDbContext())
|
||||
{
|
||||
UserId = userId,
|
||||
ChannelId = targetId,
|
||||
ServerId = guildId ?? 0,
|
||||
IsPrivate = isPrivate,
|
||||
When = time,
|
||||
Message = message,
|
||||
Type = reminderType
|
||||
};
|
||||
await ctx.GetTable<Reminder>()
|
||||
.InsertAsync(() => new Reminder
|
||||
{
|
||||
UserId = userId,
|
||||
ChannelId = targetId,
|
||||
ServerId = guildId ?? 0,
|
||||
IsPrivate = isPrivate,
|
||||
When = time,
|
||||
Message = message,
|
||||
Type = reminderType,
|
||||
DateAdded = DateTime.UtcNow
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.Set<Reminder>()
|
||||
.AddAsync(rem);
|
||||
await ctx.SaveChangesAsync();
|
||||
_tcs.SetResult(true);
|
||||
}
|
||||
|
||||
public async Task<List<Reminder>> GetServerReminders(int page, ulong guildId)
|
||||
|
@@ -110,14 +110,14 @@ public partial class Utility
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(0)]
|
||||
public Task Repeat(StoopidTime interval, [Leftover] string message)
|
||||
public Task Repeat(ParsedTimespan interval, [Leftover] string message)
|
||||
=> Repeat(ctx.Channel, null, interval, message);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(0)]
|
||||
public Task Repeat(ITextChannel channel, StoopidTime interval, [Leftover] string message)
|
||||
public Task Repeat(ITextChannel channel, ParsedTimespan interval, [Leftover] string message)
|
||||
=> Repeat(channel, null, interval, message);
|
||||
|
||||
[Cmd]
|
||||
@@ -138,14 +138,14 @@ public partial class Utility
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(2)]
|
||||
public Task Repeat(GuildDateTime? timeOfDay, StoopidTime? interval, [Leftover] string message)
|
||||
public Task Repeat(GuildDateTime? timeOfDay, ParsedTimespan? interval, [Leftover] string message)
|
||||
=> Repeat(ctx.Channel, timeOfDay, interval, message);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(3)]
|
||||
public async Task Repeat(IMessageChannel channel, GuildDateTime? timeOfDay, StoopidTime? interval,
|
||||
public async Task Repeat(IMessageChannel channel, GuildDateTime? timeOfDay, ParsedTimespan? interval,
|
||||
[Leftover] string message)
|
||||
{
|
||||
if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id)
|
||||
|
@@ -186,7 +186,7 @@ public partial class Utility : NadekoModule
|
||||
|
||||
return CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.inrole_list(roleName, roleUsers.Count)))
|
||||
.WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
|
||||
.WithDescription(string.Join("\n", pageUsers));
|
||||
})
|
||||
.SendAsync();
|
||||
|
11
src/NadekoBot/Modules/Xp/BuyResult.cs
Normal file
11
src/NadekoBot/Modules/Xp/BuyResult.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace NadekoBot.Modules.Xp.Services;
|
||||
|
||||
public enum BuyResult
|
||||
{
|
||||
Success,
|
||||
XpShopDisabled,
|
||||
AlreadyOwned,
|
||||
InsufficientFunds,
|
||||
UnknownItem,
|
||||
InsufficientPatronTier,
|
||||
}
|
@@ -51,33 +51,6 @@ public partial class Xp : NadekoModule<XpService>
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpNotify()
|
||||
{
|
||||
var globalSetting = _service.GetNotificationType(ctx.User);
|
||||
var serverSetting = _service.GetNotificationType(ctx.User.Id, ctx.Guild.Id);
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting))
|
||||
.AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting));
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpNotify(NotifyPlace place, XpNotificationLocation type)
|
||||
{
|
||||
if (place == NotifyPlace.Guild)
|
||||
await _service.ChangeNotificationType(ctx.User.Id, ctx.Guild.Id, type);
|
||||
else
|
||||
await _service.ChangeNotificationType(ctx.User, type);
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
@@ -154,9 +127,9 @@ public partial class Xp : NadekoModule<XpService>
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.exclusion_list))
|
||||
.WithDescription(string.Join('\n', items))
|
||||
.WithOkColor();
|
||||
.WithTitle(GetText(strs.exclusion_list))
|
||||
.WithDescription(string.Join('\n', items))
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
})
|
||||
@@ -214,16 +187,12 @@ public partial class Xp : NadekoModule<XpService>
|
||||
|
||||
for (var i = 0; i < users.Count; i++)
|
||||
{
|
||||
var levelStats = new LevelStats(users[i].Xp + users[i].AwardedXp);
|
||||
var levelStats = new LevelStats(users[i].Xp);
|
||||
var user = ((SocketGuild)ctx.Guild).GetUser(users[i].UserId);
|
||||
|
||||
var userXpData = users[i];
|
||||
|
||||
var awardStr = string.Empty;
|
||||
if (userXpData.AwardedXp > 0)
|
||||
awardStr = $"(+{userXpData.AwardedXp})";
|
||||
else if (userXpData.AwardedXp < 0)
|
||||
awardStr = $"({userXpData.AwardedXp})";
|
||||
|
||||
embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}",
|
||||
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
|
||||
@@ -266,8 +235,8 @@ public partial class Xp : NadekoModule<XpService>
|
||||
.Page((users, curPage) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.global_leaderboard));
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.global_leaderboard));
|
||||
|
||||
if (!users.Any())
|
||||
{
|
||||
@@ -287,6 +256,28 @@ public partial class Xp : NadekoModule<XpService>
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[Priority(1)]
|
||||
public Task XpLevelSet(int level, IGuildUser user)
|
||||
=> XpLevelSet(level, user.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[Priority(0)]
|
||||
public async Task XpLevelSet(int level, ulong userId)
|
||||
{
|
||||
if (level < 0)
|
||||
return;
|
||||
|
||||
await _service.SetLevelAsync(ctx.Guild.Id, userId, level);
|
||||
await Response()
|
||||
.Confirm(strs.level_set($"<@{userId}>", Format.Bold(level.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
@@ -351,8 +342,8 @@ public partial class Xp : NadekoModule<XpService>
|
||||
public async Task XpReset(ulong userId)
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.reset))
|
||||
.WithDescription(GetText(strs.reset_user_confirm));
|
||||
.WithTitle(GetText(strs.reset))
|
||||
.WithDescription(GetText(strs.reset_user_confirm));
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
@@ -368,8 +359,8 @@ public partial class Xp : NadekoModule<XpService>
|
||||
public async Task XpReset()
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.reset))
|
||||
.WithDescription(GetText(strs.reset_server_confirm));
|
||||
.WithTitle(GetText(strs.reset))
|
||||
.WithDescription(GetText(strs.reset_server_confirm));
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
@@ -446,20 +437,20 @@ public partial class Xp : NadekoModule<XpService>
|
||||
{
|
||||
if (!items.Any())
|
||||
return CreateEmbed()
|
||||
.WithDescription(GetText(strs.not_found))
|
||||
.WithErrorColor();
|
||||
.WithDescription(GetText(strs.not_found))
|
||||
.WithErrorColor();
|
||||
|
||||
var (key, item) = items.FirstOrDefault();
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(item.Name)
|
||||
.AddField(GetText(strs.price),
|
||||
CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()),
|
||||
true)
|
||||
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
|
||||
? item.Url
|
||||
: item.Preview);
|
||||
.WithOkColor()
|
||||
.WithTitle(item.Name)
|
||||
.AddField(GetText(strs.price),
|
||||
CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()),
|
||||
true)
|
||||
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
|
||||
? item.Url
|
||||
: item.Preview);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Desc))
|
||||
eb.AddField(GetText(strs.desc), item.Desc);
|
||||
@@ -604,15 +595,4 @@ public partial class Xp : NadekoModule<XpService>
|
||||
await _service.UseShopItemAsync(ctx.User.Id, type, key);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNotifLocationString(XpNotificationLocation loc)
|
||||
{
|
||||
if (loc == XpNotificationLocation.Channel)
|
||||
return GetText(strs.xpn_notif_channel);
|
||||
|
||||
if (loc == XpNotificationLocation.Dm)
|
||||
return GetText(strs.xpn_notif_dm);
|
||||
|
||||
return GetText(strs.xpn_notif_disabled);
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Processing;
|
||||
using System.Threading.Channels;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using LinqToDB.Tools;
|
||||
using NadekoBot.Modules.Administration;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Exception = System.Exception;
|
||||
@@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace NadekoBot.Modules.Xp.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<DiscordUser?> GetUserAsync(ulong userId);
|
||||
}
|
||||
|
||||
public sealed class UserService : IUserService, INService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
|
||||
public UserService(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<DiscordUser> GetUserAsync(ulong userId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var user = await uow
|
||||
.GetTable<DiscordUser>()
|
||||
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
{
|
||||
private readonly DbService _db;
|
||||
@@ -72,6 +48,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
|
||||
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
|
||||
private readonly IMessageSenderService _sender;
|
||||
private readonly INotifySubscriber _notifySub;
|
||||
|
||||
public XpService(
|
||||
DiscordSocketClient client,
|
||||
@@ -87,7 +64,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
XpConfigService xpConfig,
|
||||
IPubSub pubSub,
|
||||
IPatronageService ps,
|
||||
IMessageSenderService sender)
|
||||
IMessageSenderService sender,
|
||||
INotifySubscriber notifySub)
|
||||
{
|
||||
_db = db;
|
||||
_images = images;
|
||||
@@ -99,6 +77,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
_xpConfig = xpConfig;
|
||||
_pubSub = pubSub;
|
||||
_sender = sender;
|
||||
_notifySub = notifySub;
|
||||
_excludedServers = new();
|
||||
_excludedChannels = new();
|
||||
_client = client;
|
||||
@@ -159,14 +138,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniGuildXpStats
|
||||
{
|
||||
public long Xp { get; set; }
|
||||
public XpNotificationLocation NotifyOnLevelUp { get; set; }
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong UserId { get; set; }
|
||||
}
|
||||
|
||||
private async Task UpdateXp()
|
||||
{
|
||||
try
|
||||
@@ -197,9 +168,9 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
|
||||
var dus = new List<DiscordUser>(globalToAdd.Count);
|
||||
var gxps = new List<UserXpStats>(globalToAdd.Count);
|
||||
var conf = _xpConfig.Data;
|
||||
await using (var ctx = _db.GetDbContext())
|
||||
{
|
||||
var conf = _xpConfig.Data;
|
||||
if (conf.CurrencyPerXp > 0)
|
||||
{
|
||||
foreach (var user in globalToAdd)
|
||||
@@ -261,8 +232,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
GuildId = guildId,
|
||||
Xp = group.Key,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
AwardedXp = 0,
|
||||
NotifyOnLevelUp = XpNotificationLocation.None
|
||||
},
|
||||
_ => new()
|
||||
{
|
||||
@@ -300,8 +269,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
du.UserId,
|
||||
false,
|
||||
oldLevel.Level,
|
||||
newLevel.Level,
|
||||
du.NotifyOnLevelUp));
|
||||
newLevel.Level));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,8 +278,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
if (guildToAdd.TryGetValue(du.GuildId, out var users)
|
||||
&& users.TryGetValue(du.UserId, out var xpGainData))
|
||||
{
|
||||
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount + du.AwardedXp);
|
||||
var newLevel = new LevelStats(du.Xp + du.AwardedXp);
|
||||
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount);
|
||||
var newLevel = new LevelStats(du.Xp);
|
||||
|
||||
if (oldLevel.Level < newLevel.Level)
|
||||
{
|
||||
@@ -321,8 +289,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
du.UserId,
|
||||
true,
|
||||
oldLevel.Level,
|
||||
newLevel.Level,
|
||||
du.NotifyOnLevelUp));
|
||||
newLevel.Level));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,8 +306,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
ulong userId,
|
||||
bool isServer,
|
||||
long oldLevel,
|
||||
long newLevel,
|
||||
XpNotificationLocation notifyLoc)
|
||||
long newLevel)
|
||||
=> async () =>
|
||||
{
|
||||
if (isServer)
|
||||
@@ -348,7 +314,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
|
||||
}
|
||||
|
||||
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel, notifyLoc);
|
||||
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel);
|
||||
};
|
||||
|
||||
private async Task HandleRewardsInternalAsync(
|
||||
@@ -378,9 +344,45 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
if (role is not null && user is not null)
|
||||
{
|
||||
if (rrew.Remove)
|
||||
_ = user.RemoveRoleAsync(role);
|
||||
{
|
||||
try
|
||||
{
|
||||
await user.RemoveRoleAsync(role);
|
||||
await _notifySub.NotifyAsync(new RemoveRoleRewardNotifyModel(guild.Id,
|
||||
role.Id,
|
||||
user.Id,
|
||||
newLevel),
|
||||
isShardLocal: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Unable to remove role {RoleId} from user {UserId}: {Message}",
|
||||
role.Id,
|
||||
user.Id,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
_ = user.AddRoleAsync(role);
|
||||
{
|
||||
try
|
||||
{
|
||||
await user.AddRoleAsync(role);
|
||||
await _notifySub.NotifyAsync(new AddRoleRewardNotifyModel(guild.Id,
|
||||
role.Id,
|
||||
user.Id,
|
||||
newLevel),
|
||||
isShardLocal: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Unable to add role {RoleId} to user {UserId}: {Message}",
|
||||
role.Id,
|
||||
user.Id,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,59 +401,25 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
ulong channelId,
|
||||
ulong userId,
|
||||
bool isServer,
|
||||
long newLevel,
|
||||
XpNotificationLocation notifyLoc)
|
||||
long newLevel)
|
||||
{
|
||||
if (notifyLoc == XpNotificationLocation.None)
|
||||
return;
|
||||
|
||||
var guild = _client.GetGuild(guildId);
|
||||
var user = guild?.GetUser(userId);
|
||||
var ch = guild?.GetTextChannel(channelId);
|
||||
|
||||
if (guild is null || user is null)
|
||||
return;
|
||||
|
||||
if (isServer)
|
||||
{
|
||||
if (notifyLoc == XpNotificationLocation.Dm)
|
||||
var model = new LevelUpNotifyModel()
|
||||
{
|
||||
await _sender.Response(user)
|
||||
.Confirm(_strings.GetText(strs.level_up_dm(user.Mention,
|
||||
Format.Bold(newLevel.ToString()),
|
||||
Format.Bold(guild.ToString() ?? "-")),
|
||||
guild.Id))
|
||||
.SendAsync();
|
||||
}
|
||||
else // channel
|
||||
{
|
||||
if (ch is not null)
|
||||
{
|
||||
await _sender.Response(ch)
|
||||
.Confirm(_strings.GetText(strs.level_up_channel(user.Mention,
|
||||
Format.Bold(newLevel.ToString())),
|
||||
guild.Id))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
else // global level
|
||||
{
|
||||
var chan = notifyLoc switch
|
||||
{
|
||||
XpNotificationLocation.Dm => (IMessageChannel)await user.CreateDMChannelAsync(),
|
||||
XpNotificationLocation.Channel => ch,
|
||||
_ => null
|
||||
GuildId = guildId,
|
||||
UserId = userId,
|
||||
ChannelId = channelId,
|
||||
Level = newLevel
|
||||
};
|
||||
|
||||
if (chan is null)
|
||||
return;
|
||||
|
||||
await _sender.Response(chan)
|
||||
.Confirm(_strings.GetText(strs.level_up_global(user.Mention,
|
||||
Format.Bold(newLevel.ToString())),
|
||||
guild.Id))
|
||||
.SendAsync();
|
||||
await _notifySub.NotifyAsync(model, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +563,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
return await uow
|
||||
.UserXpStats
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||
.OrderByDescending(x => x.Xp)
|
||||
.Skip(page * 10)
|
||||
.Take(10)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
@@ -606,7 +574,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<UserXpStats>()
|
||||
.Where(x => x.GuildId == guildId && x.UserId.In(users))
|
||||
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||
.OrderByDescending(x => x.Xp)
|
||||
.Skip(page * 10)
|
||||
.Take(10)
|
||||
.ToArrayAsyncLinqToDB();
|
||||
@@ -635,35 +603,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
.ToArrayAsyncLinqToDB();
|
||||
}
|
||||
|
||||
public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var user = uow.GetOrCreateUserXpStats(guildId, userId);
|
||||
user.NotifyOnLevelUp = type;
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public XpNotificationLocation GetNotificationType(ulong userId, ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var user = uow.GetOrCreateUserXpStats(guildId, userId);
|
||||
return user.NotifyOnLevelUp;
|
||||
}
|
||||
|
||||
public XpNotificationLocation GetNotificationType(IUser user)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
return uow.GetOrCreateUser(user).NotifyOnLevelUp;
|
||||
}
|
||||
|
||||
public async Task ChangeNotificationType(IUser user, XpNotificationLocation type)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user);
|
||||
du.NotifyOnLevelUp = type;
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private Task Client_OnGuildAvailable(SocketGuild guild)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
@@ -903,7 +842,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
using var uow = _db.GetDbContext();
|
||||
var usr = uow.GetOrCreateUserXpStats(guildId, userId);
|
||||
|
||||
usr.AwardedXp += amount;
|
||||
usr.Xp += amount;
|
||||
|
||||
uow.SaveChanges();
|
||||
}
|
||||
@@ -949,7 +888,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
return new(du,
|
||||
stats,
|
||||
new(totalXp),
|
||||
new(stats.Xp + stats.AwardedXp),
|
||||
new(stats.Xp),
|
||||
globalRank,
|
||||
guildRank);
|
||||
}
|
||||
@@ -1192,19 +1131,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
outlinePen));
|
||||
}
|
||||
|
||||
if (stats.FullGuildStats.AwardedXp != 0 && template.User.Xp.Awarded.Show)
|
||||
{
|
||||
var sign = stats.FullGuildStats.AwardedXp > 0 ? "+ " : "";
|
||||
var awX = template.User.Xp.Awarded.Pos.X
|
||||
- (Math.Max(0, stats.FullGuildStats.AwardedXp.ToString().Length - 2) * 5);
|
||||
var awY = template.User.Xp.Awarded.Pos.Y;
|
||||
img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})",
|
||||
_fonts.NotoSans.CreateFont(template.User.Xp.Awarded.FontSize, FontStyle.Bold),
|
||||
Brushes.Solid(template.User.Xp.Awarded.Color),
|
||||
outlinePen,
|
||||
new(awX, awY)));
|
||||
}
|
||||
|
||||
var rankPen = new SolidPen(Color.White, 1);
|
||||
//ranking
|
||||
if (template.User.GlobalRank.Show)
|
||||
@@ -1671,14 +1597,27 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
|
||||
.CountAsyncLinqToDB();
|
||||
}
|
||||
}
|
||||
|
||||
public enum BuyResult
|
||||
{
|
||||
Success,
|
||||
XpShopDisabled,
|
||||
AlreadyOwned,
|
||||
InsufficientFunds,
|
||||
UnknownItem,
|
||||
InsufficientPatronTier,
|
||||
public async Task SetLevelAsync(ulong guildId, ulong userId, int level)
|
||||
{
|
||||
var lvlStats = LevelStats.CreateForLevel(level);
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.GetTable<UserXpStats>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
UserId = userId,
|
||||
Xp = lvlStats.TotalXp,
|
||||
DateAdded = DateTime.UtcNow
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
Xp = lvlStats.TotalXp
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
UserId = userId
|
||||
});
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>5.2.3</Version>
|
||||
<Version>5.3.2</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
@@ -29,7 +29,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1"/>
|
||||
<PackageReference Include="Discord.Net" Version="3.15.3"/>
|
||||
<PackageReference Include="Discord.Net" Version="3.16.0" />
|
||||
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246"/>
|
||||
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
|
||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
|
||||
|
2
src/NadekoBot/NadekoBot.csproj.DotSettings
Normal file
2
src/NadekoBot/NadekoBot.csproj.DotSettings
Normal file
@@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cadministration_005Cnotify_005Cmodels/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@@ -3,8 +3,8 @@ namespace NadekoBot.Common;
|
||||
|
||||
public enum AddRemove
|
||||
{
|
||||
Add = int.MinValue,
|
||||
Remove = int.MinValue + 1,
|
||||
Rem = int.MinValue + 1,
|
||||
Rm = int.MinValue + 1
|
||||
Add = 0,
|
||||
Remove = 1,
|
||||
Rem = 1,
|
||||
Rm = 1,
|
||||
}
|
@@ -13,7 +13,7 @@ namespace NadekoBot.Common.Configs;
|
||||
public sealed partial class BotConfig : ICloneable<BotConfig>
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; } = 8;
|
||||
public int Version { get; set; } = 9;
|
||||
|
||||
[Comment("""
|
||||
Most commands, when executed, have a small colored line
|
||||
@@ -82,6 +82,9 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
|
||||
[Comment("""List of modules and commands completely blocked on the bot""")]
|
||||
public BlockedConfig Blocked { get; set; }
|
||||
|
||||
[Comment("""List of modules and commands blocked from usage in DMs on the bot""")]
|
||||
public BlockedConfig DmBlocked { get; set; } = new();
|
||||
|
||||
[Comment("""Which string will be used to recognize the commands""")]
|
||||
public string Prefix { get; set; }
|
||||
|
||||
|
@@ -157,6 +157,9 @@ public sealed class DoAsUserMessage : IUserMessage
|
||||
public MessageCallData? CallData
|
||||
=> _msg.CallData;
|
||||
|
||||
public IReadOnlyCollection<MessageSnapshot> ForwardedMessages
|
||||
=> _msg.ForwardedMessages;
|
||||
|
||||
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
|
||||
{
|
||||
return _msg.ModifyAsync(func, options);
|
||||
|
@@ -51,7 +51,7 @@ public class NadekoEmbedBuilder : EmbedBuilder
|
||||
var bcColors = bcsData.Data.Color;
|
||||
_okColor = guildColors?.Ok ?? bcColors.Ok.ToDiscordColor();
|
||||
_errorColor = guildColors?.Error ?? bcColors.Error.ToDiscordColor();
|
||||
_pendingColor = guildColors?.Pending ?? bcColors.Pending.ToDiscordColor();
|
||||
_pendingColor = guildColors?.Warn ?? bcColors.Pending.ToDiscordColor();
|
||||
}
|
||||
|
||||
public EmbedBuilder WithOkColor()
|
||||
|
8
src/NadekoBot/_common/Services/IUserService.cs
Normal file
8
src/NadekoBot/_common/Services/IUserService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Xp.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<DiscordUser?> GetUserAsync(ulong userId);
|
||||
}
|
@@ -108,7 +108,7 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN
|
||||
{
|
||||
_colors[guildId] = _colors[guildId] with
|
||||
{
|
||||
Pending = color?.ToDiscordColor()
|
||||
Warn = color?.ToDiscordColor()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -126,8 +126,8 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN
|
||||
{
|
||||
var colors = new Colors(
|
||||
ConvertColor(color.OkColor),
|
||||
ConvertColor(color.ErrorColor),
|
||||
ConvertColor(color.PendingColor));
|
||||
ConvertColor(color.PendingColor),
|
||||
ConvertColor(color.ErrorColor));
|
||||
|
||||
_colors.TryAdd(color.GuildId, colors);
|
||||
}
|
||||
|
@@ -10,4 +10,4 @@ public interface IGuildColorsService
|
||||
Task SetPendingColor(ulong guildId, Rgba32? color);
|
||||
}
|
||||
|
||||
public record struct Colors(Color? Ok, Color? Pending, Color? Error);
|
||||
public record struct Colors(Color? Ok, Color? Warn, Color? Error);
|
24
src/NadekoBot/_common/Services/UserService.cs
Normal file
24
src/NadekoBot/_common/Services/UserService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Xp.Services;
|
||||
|
||||
public sealed class UserService : IUserService, INService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
|
||||
public UserService(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<DiscordUser?> GetUserAsync(ulong userId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var user = await uow
|
||||
.GetTable<DiscordUser>()
|
||||
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
@@ -70,10 +70,10 @@ public sealed class BotConfigService : ConfigServiceBase<BotConfig>
|
||||
c.IgnoreOtherBots = true;
|
||||
});
|
||||
|
||||
if(data.Version < 8)
|
||||
if(data.Version < 9)
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 8;
|
||||
c.Version = 9;
|
||||
});
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace NadekoBot.Common.TypeReaders.Models;
|
||||
|
||||
public class StoopidTime
|
||||
public class ParsedTimespan
|
||||
{
|
||||
private static readonly Regex _regex = new(
|
||||
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
|
||||
@@ -11,9 +11,9 @@ public class StoopidTime
|
||||
public string Input { get; set; } = string.Empty;
|
||||
public TimeSpan Time { get; set; } = default;
|
||||
|
||||
private StoopidTime() { }
|
||||
private ParsedTimespan() { }
|
||||
|
||||
public static StoopidTime FromInput(string input)
|
||||
public static ParsedTimespan FromInput(string input)
|
||||
{
|
||||
var m = _regex.Match(input);
|
||||
|
||||
@@ -52,10 +52,10 @@ public class StoopidTime
|
||||
};
|
||||
}
|
||||
|
||||
public static implicit operator TimeSpan?(StoopidTime? st)
|
||||
public static implicit operator TimeSpan?(ParsedTimespan? st)
|
||||
=> st?.Time;
|
||||
|
||||
public static implicit operator StoopidTime(TimeSpan ts)
|
||||
public static implicit operator ParsedTimespan(TimeSpan ts)
|
||||
=> new()
|
||||
{
|
||||
Input = ts.ToString(),
|
||||
|
@@ -3,20 +3,20 @@ using NadekoBot.Common.TypeReaders.Models;
|
||||
|
||||
namespace NadekoBot.Common.TypeReaders;
|
||||
|
||||
public sealed class StoopidTimeTypeReader : NadekoTypeReader<StoopidTime>
|
||||
public sealed class StoopidTimeTypeReader : NadekoTypeReader<ParsedTimespan>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<StoopidTime>> ReadAsync(ICommandContext context, string input)
|
||||
public override ValueTask<TypeReaderResult<ParsedTimespan>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Unsuccessful, "Input is empty."));
|
||||
return new(TypeReaderResult.FromError<ParsedTimespan>(CommandError.Unsuccessful, "Input is empty."));
|
||||
try
|
||||
{
|
||||
var time = StoopidTime.FromInput(input);
|
||||
var time = ParsedTimespan.FromInput(input);
|
||||
return new(TypeReaderResult.FromSuccess(time));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Exception, ex.Message));
|
||||
return new(TypeReaderResult.FromError<ParsedTimespan>(CommandError.Exception, ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
@@ -715,6 +715,8 @@ color:
|
||||
avatar:
|
||||
- avatar
|
||||
- av
|
||||
banner:
|
||||
- banner
|
||||
translate:
|
||||
- translate
|
||||
- trans
|
||||
@@ -1039,6 +1041,12 @@ gamevoicechannel:
|
||||
- gvc
|
||||
shoplistadd:
|
||||
- shoplistadd
|
||||
dmcommand:
|
||||
- dmcommand
|
||||
- dmcmd
|
||||
dmmodule:
|
||||
- dmmodule
|
||||
- dmmod
|
||||
globalcommand:
|
||||
- globalcommand
|
||||
- gcmd
|
||||
@@ -1089,9 +1097,6 @@ xpexclusionlist:
|
||||
xpexclude:
|
||||
- xpexclude
|
||||
- xpex
|
||||
xpnotify:
|
||||
- xpnotify
|
||||
- xpn
|
||||
xpleveluprewards:
|
||||
- xplvluprewards
|
||||
- xprews
|
||||
@@ -1121,6 +1126,8 @@ xpshopbuy:
|
||||
- xpshopbuy
|
||||
xpshopuse:
|
||||
- xpshopuse
|
||||
xplevelset:
|
||||
- xplevelset
|
||||
clubcreate:
|
||||
- clubcreate
|
||||
clubtransfer:
|
||||
@@ -1533,4 +1540,23 @@ servercolorpending:
|
||||
- pending
|
||||
- warn
|
||||
- warning
|
||||
- pend
|
||||
- pend
|
||||
minesweeper:
|
||||
- minesweeper
|
||||
- mw
|
||||
temprole:
|
||||
- temprole
|
||||
notify:
|
||||
- notify
|
||||
- nfy
|
||||
notifylist:
|
||||
- notifylist
|
||||
- notifyl
|
||||
notifyclear:
|
||||
- notifyclear
|
||||
- notifyremove
|
||||
- notifyrm
|
||||
- notifclr
|
||||
winlb:
|
||||
- winlb
|
||||
- wins
|
@@ -1,5 +1,5 @@
|
||||
# DO NOT CHANGE
|
||||
version: 8
|
||||
version: 9
|
||||
# Most commands, when executed, have a small colored line
|
||||
# next to the response. The color depends whether the command
|
||||
# is completed, errored or in progress (pending)
|
||||
@@ -78,6 +78,10 @@ helpText: |-
|
||||
blocked:
|
||||
commands: []
|
||||
modules: []
|
||||
# List of modules and commands blocked from usage in DMs on the bot
|
||||
dmBlocked:
|
||||
commands: []
|
||||
modules: []
|
||||
# Which string will be used to recognize the commands
|
||||
prefix: .
|
||||
# Whether the bot will rotate through all specified statuses.
|
||||
|
@@ -961,6 +961,56 @@
|
||||
"MuteMembers Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".notify",
|
||||
".nfy"
|
||||
],
|
||||
"Description": "Sends a message to the current channel once the specified event occurs.\nProvide no parameters to see all available events.",
|
||||
"Usage": [
|
||||
".notify levelup Congratulations to user %user.name% for reaching level %event.level%"
|
||||
],
|
||||
"Submodule": "NotifyCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".notifylist",
|
||||
".notifyl"
|
||||
],
|
||||
"Description": "Lists all active notifications in this server.",
|
||||
"Usage": [
|
||||
".notifylist"
|
||||
],
|
||||
"Submodule": "NotifyCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".notifyclear",
|
||||
".notifyremove",
|
||||
".notifyrm",
|
||||
".notifclr"
|
||||
],
|
||||
"Description": "Removes the specified notify event.",
|
||||
"Usage": [
|
||||
".notifyclear levelup"
|
||||
],
|
||||
"Submodule": "NotifyCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".dpo"
|
||||
@@ -1523,6 +1573,22 @@
|
||||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".temprole"
|
||||
],
|
||||
"Description": "Grants a user a temporary role for the specified number of time.\nThe role must exist and be lower in the role hierarchy than your highest role.",
|
||||
"Usage": [
|
||||
".temprole 15m @User Jail",
|
||||
".temprole 7d @Newbie Trial Member"
|
||||
],
|
||||
"Submodule": "RoleCommands",
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".iam"
|
||||
@@ -1648,9 +1714,9 @@
|
||||
".sar excl",
|
||||
".sar tesar"
|
||||
],
|
||||
"Description": "Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.",
|
||||
"Description": "Toggles the sar group as exclusive.\nWhile enabled, users can only have one self-assignable role from that group.",
|
||||
"Usage": [
|
||||
".sar exclusive"
|
||||
".sar exclusive 1"
|
||||
],
|
||||
"Submodule": "sar",
|
||||
"Module": "Administration",
|
||||
@@ -3274,6 +3340,21 @@
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".winlb",
|
||||
".wins"
|
||||
],
|
||||
"Description": "Shows the biggest wins leaderboard",
|
||||
"Usage": [
|
||||
".winlb",
|
||||
".winlb 5"
|
||||
],
|
||||
"Submodule": "BetStatsCommands",
|
||||
"Module": "Gambling",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".gamblestats",
|
||||
@@ -3915,6 +3996,20 @@
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".minesweeper",
|
||||
".mw"
|
||||
],
|
||||
"Description": "Creates a spoiler-based minesweeper mini game.\nYou may specify the number of mines.",
|
||||
"Usage": [
|
||||
".minesweeper 15"
|
||||
],
|
||||
"Submodule": "Games",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".acrophobia",
|
||||
@@ -5522,6 +5617,38 @@
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".dmmodule",
|
||||
".dmmod"
|
||||
],
|
||||
"Description": "Toggles whether a module can be used in DMs.",
|
||||
"Usage": [
|
||||
".dmmodule Gambling"
|
||||
],
|
||||
"Submodule": "GlobalPermissionCommands",
|
||||
"Module": "Permissions",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".dmcommand",
|
||||
".dmcmd"
|
||||
],
|
||||
"Description": "Toggles whether a command can be used in DMs.",
|
||||
"Usage": [
|
||||
".dmcommand .stats"
|
||||
],
|
||||
"Submodule": "GlobalPermissionCommands",
|
||||
"Module": "Permissions",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".resetperms"
|
||||
@@ -5791,6 +5918,19 @@
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".banner"
|
||||
],
|
||||
"Description": "Shows a mentioned person's banner.",
|
||||
"Usage": [
|
||||
".banner @Someone"
|
||||
],
|
||||
"Submodule": "Searches",
|
||||
"Module": "Searches",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".wikia",
|
||||
@@ -7778,21 +7918,6 @@
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".xpnotify",
|
||||
".xpn"
|
||||
],
|
||||
"Description": "Sets how the bot should notify you when you get a `server` or `global` level. This is a personal setting and affects only how you receive Global or Server level-up notifications. You can set `dm` (for the bot to send you a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable.",
|
||||
"Usage": [
|
||||
".xpnotify global dm",
|
||||
".xpnotify server channel"
|
||||
],
|
||||
"Submodule": "Xp",
|
||||
"Module": "Xp",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".xpexclude",
|
||||
@@ -7854,6 +7979,21 @@
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".xplevelset"
|
||||
],
|
||||
"Description": "Sets the level of the user you specify.",
|
||||
"Usage": [
|
||||
".xplevelset 10 @User"
|
||||
],
|
||||
"Submodule": "Xp",
|
||||
"Module": "Xp",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".xpadd"
|
||||
|
@@ -368,7 +368,7 @@ sargroupname:
|
||||
- group:
|
||||
desc: "The group number"
|
||||
name:
|
||||
desc: "The name to assign."
|
||||
desc: "The optional name to assign, clears the name if no name is provided."
|
||||
sargroupdelete:
|
||||
desc: "Deletes a self-assignable role group"
|
||||
ex:
|
||||
@@ -377,11 +377,14 @@ sargroupdelete:
|
||||
- group:
|
||||
desc: "The number of the group to delete."
|
||||
sarexclusive:
|
||||
desc: Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.
|
||||
desc: |-
|
||||
Toggles the sar group as exclusive.
|
||||
While enabled, users can only have one self-assignable role from that group.
|
||||
ex:
|
||||
- ''
|
||||
- '1'
|
||||
params:
|
||||
- { }
|
||||
- group:
|
||||
desc: "The number of the group to set exclusive."
|
||||
sarrolelevelreq:
|
||||
desc: Set a level requirement on a self-assignable role.
|
||||
ex:
|
||||
@@ -2170,6 +2173,13 @@ avatar:
|
||||
params:
|
||||
- usr:
|
||||
desc: "The user whose avatar is being displayed."
|
||||
banner:
|
||||
desc: Shows a mentioned person's banner.
|
||||
ex:
|
||||
- '@Someone'
|
||||
params:
|
||||
- usr:
|
||||
desc: "The user whose banner is being displayed."
|
||||
translate:
|
||||
desc: Translates text from the given language to the destination language.
|
||||
ex:
|
||||
@@ -3377,14 +3387,28 @@ globalcommand:
|
||||
- '{0}stats'
|
||||
params:
|
||||
- cmd:
|
||||
desc: "The type of command or expression being toggled."
|
||||
desc: "The command or expression being toggled."
|
||||
globalmodule:
|
||||
desc: Toggles whether a module can be used on any server.
|
||||
ex:
|
||||
- 'Gambling'
|
||||
params:
|
||||
- module:
|
||||
desc: "The type of module or configuration information being toggled."
|
||||
desc: "The module being toggled."
|
||||
dmcommand:
|
||||
desc: Toggles whether a command can be used in DMs.
|
||||
ex:
|
||||
- '{0}stats'
|
||||
params:
|
||||
- cmd:
|
||||
desc: "The command or expression being toggled."
|
||||
dmmodule:
|
||||
desc: Toggles whether a module can be used in DMs.
|
||||
ex:
|
||||
- 'Gambling'
|
||||
params:
|
||||
- module:
|
||||
desc: "The module being toggled."
|
||||
globalpermlist:
|
||||
desc: Lists global permissions set by the bot owner.
|
||||
ex:
|
||||
@@ -3515,17 +3539,6 @@ xpexclude:
|
||||
desc: "The ID of the channel to exclude from XP tracking."
|
||||
channel:
|
||||
desc: "The ID of the channel to exclude from XP tracking."
|
||||
xpnotify:
|
||||
desc: Sets how the bot should notify you when you get a `server` or `global` level. This is a personal setting and affects only how you receive Global or Server level-up notifications. You can set `dm` (for the bot to send you a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable.
|
||||
ex:
|
||||
- global dm
|
||||
- server channel
|
||||
params:
|
||||
- { }
|
||||
- place:
|
||||
desc: "The location where notifications should be sent, such as a specific channel or DM."
|
||||
type:
|
||||
desc: "The location where notifications for server and global level-ups should be sent."
|
||||
xpleveluprewards:
|
||||
desc: Shows currently set level up rewards.
|
||||
ex:
|
||||
@@ -4821,3 +4834,74 @@ servercolorpending:
|
||||
params:
|
||||
- color:
|
||||
desc: "The hex of the color to set"
|
||||
xplevelset:
|
||||
desc: |-
|
||||
Sets the level of the user you specify.
|
||||
ex:
|
||||
- '10 @User'
|
||||
params:
|
||||
- level:
|
||||
desc: "The level to set the user to."
|
||||
- user:
|
||||
desc: "The user to set the level of."
|
||||
temprole:
|
||||
desc: |-
|
||||
Grants a user a temporary role for the specified number of time.
|
||||
The role must exist and be lower in the role hierarchy than your highest role.
|
||||
ex:
|
||||
- '15m @User Jail'
|
||||
- '7d @Newbie Trial Member'
|
||||
params:
|
||||
- days:
|
||||
desc: "The time after which the role is automatically removed."
|
||||
- user:
|
||||
desc: "The user to give the role to."
|
||||
- role:
|
||||
desc: "The role to give to the user."
|
||||
minesweeper:
|
||||
desc: |-
|
||||
Creates a spoiler-based minesweeper mini game.
|
||||
You may specify the number of mines.
|
||||
ex:
|
||||
- '15'
|
||||
params:
|
||||
- mines:
|
||||
desc: "The number of mines to create."
|
||||
notify:
|
||||
desc: |-
|
||||
Sends a message to the current channel once the specified event occurs.
|
||||
Provide no parameters to see all available events.
|
||||
ex:
|
||||
- 'levelup Congratulations to user %user.name% for reaching level %event.level%'
|
||||
params:
|
||||
- { }
|
||||
- event:
|
||||
desc: "The event to notify on."
|
||||
- event:
|
||||
desc: "The event to notify on."
|
||||
message:
|
||||
desc: "The message to send."
|
||||
notifylist:
|
||||
desc: |-
|
||||
Lists all active notifications in this server.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
notifyclear:
|
||||
desc: |-
|
||||
Removes the specified notify event.
|
||||
ex:
|
||||
- 'levelup'
|
||||
params:
|
||||
- event:
|
||||
desc: "The notify event to clear."
|
||||
winlb:
|
||||
desc: |-
|
||||
Shows the biggest wins leaderboard
|
||||
ex:
|
||||
- ''
|
||||
- '5'
|
||||
params:
|
||||
- page:
|
||||
desc: "The optional page to display."
|
@@ -758,6 +758,10 @@
|
||||
"gcmd_remove": "Command {0} has been enabled on all servers.",
|
||||
"gmod_add": "Module {0} has been disabled on all servers.",
|
||||
"gmod_remove": "Module {0} has been enabled on all servers.",
|
||||
"dmmod_add": "Module {0} has been disabled in bot DMs.",
|
||||
"dmmod_remove": "Module {0} has been enabled in bot DMs.",
|
||||
"dmcmd_add": "Command {0} has been disabled in bot DMs.",
|
||||
"dmcmd_remove": "Command {0} has been enabled in bot DMs.",
|
||||
"lgp_none": "No blocked commands or modules.",
|
||||
"cant_read_or_send": "You can't read from or send messages to that channel.",
|
||||
"quotes_notfound": "No quotes found matching the quote ID specified.",
|
||||
@@ -1083,7 +1087,7 @@
|
||||
"giveaway_starting": "Starting giveaway...",
|
||||
"winner": "Winner",
|
||||
"giveaway_list": "List of active giveways",
|
||||
"todo_list_empty": "Your todo list is empty." ,
|
||||
"todo_list_empty": "Your todo list is empty.",
|
||||
"todo_list": "Todo List",
|
||||
"todo_stats": "{0} items | {1} completed | {2} remaining",
|
||||
"todo_add_max_limit": "You'reached the maximum amount of todos you can have.",
|
||||
@@ -1098,7 +1102,7 @@
|
||||
"search_results": "Search results",
|
||||
"queue_search_results": "Type the number of the search result to queue up that track.",
|
||||
"overloads": "Overloads",
|
||||
"honeypot_on": "Honeypot enabled on this channel." ,
|
||||
"honeypot_on": "Honeypot enabled on this channel.",
|
||||
"honeypot_off": "Honeypot disabled.",
|
||||
"afk_set": "AFK message set. Type a message in any channel to clear.",
|
||||
"rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel.",
|
||||
@@ -1111,7 +1115,7 @@
|
||||
"nc_hint": "Use `{0}nczoom x y` command to zoom in. Image is {1}x{2} pixels.",
|
||||
"nc_insuff_payment": "Invalid payment.",
|
||||
"invalid_img_size": "Image must to be {0}x{1} pixels.",
|
||||
"no_attach_found": "No attachment found. Please send the image along with this command." ,
|
||||
"no_attach_found": "No attachment found. Please send the image along with this command.",
|
||||
"trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.",
|
||||
"trfl_disabled": "Flag translation disabled.",
|
||||
"rakeback_claimed": "You've claimed {0} as rakeback!",
|
||||
@@ -1121,10 +1125,10 @@
|
||||
"self_assign_group_role_req": "Users can now self-assign a role from group {0} only if they have {1} role.",
|
||||
"sar_group_not_found": "Group with that number doesn't exist.",
|
||||
"sar_group_deleted": "Group {0} deleted.",
|
||||
"choose_one": "Choose one" ,
|
||||
"choose_one": "Choose one",
|
||||
"requires_role": "Requires role: {0}",
|
||||
"invalid_message_id": "Invalid Message Id.",
|
||||
"invalid_message_link": "The message link must be from this server.",
|
||||
"invalid_message_link": "The message link must be this Bot's message. The bot can't add buttons to other users' messages.",
|
||||
"btnrole_message_max": "Limit reached. You may have up to 25 button roles per message.",
|
||||
"btnrole_not_found": "No button role found on that message.",
|
||||
"btnrole_none": "There are no button roles on this page.",
|
||||
@@ -1135,5 +1139,25 @@
|
||||
"no_last_queued_found": "No last queued track found.",
|
||||
"wrongsong_success": "Oops! Wrong song removed: {0}",
|
||||
"server_not_found": "Server not found.",
|
||||
"server_color_set": "Successfully set a new server color."
|
||||
"server_color_set": "Successfully set a new server color.",
|
||||
"lasts_until": "Lasts Until",
|
||||
"winners_count": "Winners",
|
||||
"level_set": "Level of user {0} set to {1} on this server.",
|
||||
"temp_role_added": "User {0} has been given {1} role temporarily. The role expires {2}",
|
||||
"user_afk": "User {0} is AFK.",
|
||||
"notify_on": "Notification message will be sent in {0} channel when {1} event triggers.",
|
||||
"notify_off": "Notification message will no longer be sent when {0} event triggers.",
|
||||
"notify_none": "No notifications on this page.",
|
||||
"notify_msg_not_set": "Notification message is not set for this event.",
|
||||
"notify_list": "Notify List",
|
||||
"notify_type": "Type",
|
||||
"notify_msg": "Notify Message",
|
||||
"notify_available": "List of available notify events",
|
||||
"notify_desc_levelup": "Triggers when a user levels up on this server.",
|
||||
"notify_desc_protection": "Triggers when antialt, antispam or antiraid is triggered.",
|
||||
"notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).",
|
||||
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
|
||||
"notify_desc_not_found": "No description found for this notify event. Please report this.",
|
||||
"winlb": "Biggest Wins Leaderboard",
|
||||
"no_banner": "No banner set."
|
||||
}
|
||||
|
Reference in New Issue
Block a user