Compare commits

...

39 Commits
5.2.0 ... 5.3.3

Author SHA1 Message Date
Kwoth
7ba345b0fc docs: updated changelog, updated version 2024-12-15 13:18:51 +00:00
Kwoth
5d775c9589 Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2024-12-15 13:14:16 +00:00
Kwoth
2bd8ead10c Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2024-12-15 13:13:59 +00:00
Kwoth
1148ba3e6e Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2024-12-14 15:33:23 +00:00
Kwoth
5498bec8cc fix: .banner fixed, will now show guild banner if available, otherwise global banner, if available 2024-12-14 15:33:09 +00:00
Kwoth
9eed0c6be5 fix: .banner fixed, will now show guild banner if available, otherwise global banner, if available 2024-12-14 15:31:05 +00:00
Kwoth
acf6b7cf58 docs: updated changelog, updated version 2024-12-13 08:28:55 +00:00
Kwoth
8598419c5f fix: .dmcmd will now correctly block commands in dms, not globally
change: timely will no longer require guild context, as dmcmd .timely will do the same thing
2024-12-13 08:22:13 +00:00
Kwoth
758093eb32 fix: bannersize fixed, honeypot will now put 'Honeypot' as a ban reason 2024-12-12 08:56:45 +00:00
Kwoth
f44dd03f1a fix: .sclr will now correctly show the color without alpha prefixed 2024-12-11 23:06:01 +00:00
Kwoth
8ac5ec9f57 fix: .banner will now show a 'no banner' error, and will use image embed instead of thumbnail to show the banner. Only user server banners will be shown for now, global user banners will still not work. 2024-12-11 23:00:59 +00:00
Kwoth
5209ba802a change: winlb now has 9 items per page to look not broken 2024-12-11 17:21:25 +00:00
Kwoth
adfce6670c change: winlb now has a title 2024-12-11 17:13:59 +00:00
Kwoth
f8fbc71985 change: added role icon to .inrole, .winlb will now show userids when user can't be found 2024-12-10 14:16:24 +00:00
Kwoth
d4e2516a17 change: winlb embed fields are now inline to use less space 2024-12-10 11:24:02 +00:00
Kwoth
f62a67e2e6 docs: Updated commandlist 2024-12-10 07:20:47 +00:00
Kwoth
7895b5e702 fix: .translate will now use 2 embeds 2024-12-10 07:17:49 +00:00
Kwoth
28c8ccfb5f fix: .sar ex had an outdated description 2024-12-10 07:12:10 +00:00
Kwoth
79026e0c27 fix: .winlb looks better when there are no items 2024-12-09 17:31:23 +00:00
Kwoth
8b228b840a docs: slight docs improvement, added windows from source guide in the menu, separated it from the windows installer guide 2024-12-09 14:54:52 +00:00
Kwoth
f61c1a159d docs: fixed docs 2024-12-09 14:43:55 +00:00
Kwoth
2cdcdb2b23 dev: Fix tests 2024-12-07 16:54:18 +00:00
Kwoth
8d0a3ecb20 docs: Updated changelog.md 2024-12-07 16:51:02 +00:00
Kwoth
97f1405a94 add: added addrolereward and removerolereward events for .notify
add: added .notify with no params showing events with descriptions
add: .winlb
docs: updated docs
dev: updated discord.net, redid migrations
2024-12-07 16:46:06 +00:00
Kwoth
0622236523 add: Added .notify and migrations, added levelup and protection events for notify, removed xpnotify completely 2024-12-05 14:35:42 +00:00
Kwoth
0f240925e8 add: notify, minesweeper, migrations
dev: renames, refactors
change: remind optimized wait
2024-12-03 14:13:31 +00:00
Kwoth
d583e2b99a change: QUeueing a song after the queue is finished will restart the playback 2024-12-02 03:21:43 +00:00
Kwoth
2d72f6f498 add: Added .dmmod and .dmcmd to disable modules and commands in bot DMs 2024-12-02 02:20:33 +00:00
Kwoth
5143e42dff add: Added .xplevelset
remove: removed awardedxp from database.
change: .sclr show will now show hex
change: .awardxp will now add directly to user's real xp
2024-12-01 03:57:31 +00:00
Kwoth
790c36df8d fix: .setstream and .setactivity will now pause .ropl 2024-11-30 10:02:08 +00:00
Kwoth
15709bc8fb change: Finished giveaway will now reply to the giveaway message and ping a winner 2024-11-29 08:52:47 +00:00
Kwoth
f280f72227 fix: fixed giveaway load broken in the last patch 2024-11-29 08:50:55 +00:00
Kwoth
4069368beb change: added ending date for givaway as a timestamp tag
fixed: fixed an issue with flag translates
2024-11-28 08:55:53 +00:00
Kwoth
e036a2d3c9 fix: fixed .sclr again, again, and fixed .iamn 2024-11-27 03:26:57 +00:00
Kwoth
62a16f3faf fix: fixed .iam
fix: fixed .sclr not being respected on many different commands
change: .rps now also has the amount bet
2024-11-27 02:41:05 +00:00
Kwoth
231451f978 fix: fixed .sclr again 2024-11-27 01:28:17 +00:00
Kwoth
2bed4e7eac fix: Fixed many issues with 5.2.0 2024-11-27 01:23:38 +00:00
Kwoth
22eea3fa7b docs: updated changelog 2024-11-26 23:49:32 +00:00
Kwoth
8b1767078e fix: fixed sar migration 2024-11-26 23:31:27 +00:00
94 changed files with 10039 additions and 859 deletions

View File

@@ -2,52 +2,166 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.3.3] - 15.12.2024
## Fixed
- `.notify` commands are no longer owner only, they now require Admin permissions
- `.notify` messages can now mention anyone
## [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
- `.iam` Fixed
- `.sclr` will now properly change color on many commands it didn't work previously
### Changed
- `.rps` now also has bet amount in the result, like other gambling commands
## [5.2.2] - 27.11.2024
### Changed
- Button roles are now non-exclusive by default
### Fixed
- Fixed sar migration, again (this time correctly)
- Fixed `.sclr` not updating unless bot is restarted, the changes should be immediate now for warn and error
- Fixed group buttons exclusivity message always saying groups are exclusive
## [5.2.1] - 26.11.2024
### Fixed
- Fixed old self assigned missing
## [5.2.0] - 26.11.2024
### Added
- 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%
@@ -61,9 +175,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
@@ -73,9 +187,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
@@ -94,7 +208,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

View File

@@ -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

View File

@@ -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.
![Create a bot application and copy token to creds.yml file](https://cdn.nadeko.bot/tutorial/bot-creds-guide.gif)
- 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)*.

View File

@@ -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]

View File

@@ -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 @@
![Create a new bot](https://i.imgur.com/JxtRk9e.png "Create a new bot")
- Click on **`DOWNLOAD`** at the lower right
![Bot Setup](https://i.imgur.com/HqAl36p.png "Bot Setup")
- **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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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)));
}

View File

@@ -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; }

View 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,
}

View 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; }
}

View File

@@ -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; }
}

View File

@@ -1,4 +1,4 @@
namespace NadekoBot.Db.Models;
namespace NadekoBot.Db.Models;
public enum XpNotificationLocation
{

View File

@@ -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

View File

@@ -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("""
@@ -21,14 +31,13 @@ public static class MigrationQueries
INNER JOIN GuildConfigs as GC ON GN.GuildConfigId = GC.Id;
INSERT INTO Sar (GuildId, RoleId, SarGroupId, LevelReq)
SELECT SAR.GuildId, SAR.RoleId, MIN(SG2.Id), MIN(SAR.LevelRequirement)
FROM SelfAssignableRoles as SAR
INNER JOIN (SELECT GuildId FROM GroupName as gn
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
) as SG
ON SG.GuildId = SAR.GuildId
INNER JOIN GroupName as SG2
ON SG2.Number = SAR."Group"
SELECT SAR.GuildId, SAR.RoleId, (SELECT Id FROM SarGroup WHERE SG.Number = SarGroup.GroupNumber AND SG.GuildId = SarGroup.GuildId), MIN(SAR.LevelRequirement)
FROM SelfAssignableRoles as SAR
INNER JOIN (SELECT GuildId, gn.Number FROM GroupName as gn
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
) as SG
ON SG.GuildId = SAR.GuildId
WHERE SG.Number IN (SELECT GroupNumber FROM SarGroup WHERE Sar.GuildId = SarGroup.GuildId)
GROUP BY SAR.GuildId, SAR.RoleId;
INSERT INTO SarAutoDelete (GuildId, IsEnabled)

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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");

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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();

View File

@@ -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();
});
}

View File

@@ -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)

View File

@@ -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
{

View 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;
}
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules.Administration;
public interface INotifySubscriber
{
Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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]
[UserPerm(GuildPerm.Administrator)]
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]
[UserPerm(GuildPerm.Administrator)]
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]
[UserPerm(GuildPerm.Administrator)]
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]
[UserPerm(GuildPerm.Administrator)]
public async Task NotifyClear(NotifyType nType)
{
await _service.DisableAsync(ctx.Guild.Id, nType);
await Response().Confirm(strs.notify_off(nType)).SendAsync();
}
}
}

View File

@@ -0,0 +1,6 @@
namespace NadekoBot.Modules.Administration;
public static class NotifyKeys
{
public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
}

View File

@@ -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);
}

View File

@@ -0,0 +1,227 @@
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)
.Sanitize(false)
.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();
}
}

View File

@@ -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]

View File

@@ -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;

View File

@@ -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));
}
}
}

View File

@@ -283,7 +283,13 @@ public partial class Administration
{
var res = await _service.SetExclusiveButtonRoles(ctx.Guild.Id, messageId, exclusive.Value);
if (res)
if (!res)
{
await Response().Error(strs.btnrole_not_found).SendAsync();
return;
}
if (exclusive.Value)
{
await Response().Confirm(strs.btnrole_exclusive).SendAsync();
}

View File

@@ -126,7 +126,11 @@ public sealed class ButtonRolesService : INService, IReadyExecutor
Emote = emoteStr,
Label = string.Empty,
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}",
Exclusive = (uow.GetTable<ButtonRole>().Where(x => x.GuildId == guildId && x.MessageId == messageId).All(x => x.Exclusive))
Exclusive = uow.GetTable<ButtonRole>()
.Any(x => x.GuildId == guildId && x.MessageId == messageId)
&& uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.All(x => x.Exclusive)
},
_ => new()
{
@@ -178,7 +182,6 @@ public sealed class ButtonRolesService : INService, IReadyExecutor
.UpdateAsync((_) => new()
{
Exclusive = exclusive
})
> 0;
}) > 0;
}
}

View File

@@ -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();
}
}
}

View 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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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.

View File

@@ -21,7 +21,7 @@ public partial class Administration
{
var guildUser = (IGuildUser)ctx.User;
var group = await _service.GetRoleGroup(ctx.User.Id, role.Id);
var group = await _service.GetRoleGroup(ctx.Guild.Id, role.Id);
IUserMessage msg = null;
try
@@ -84,13 +84,13 @@ 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;
}
var group = await _service.GetRoleGroup(role.Guild.Id, role.Id);
var group = await _service.GetRoleGroup(ctx.Guild.Id, role.Id);
if (group is null || group.Roles.All(x => x.RoleId != role.Id))
{

View File

@@ -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()

View File

@@ -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();

View File

@@ -135,7 +135,6 @@ public partial class Gambling : GamblingModule<GamblingService>
});
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timely()
{
var val = Config.Timely.Amount;
@@ -390,7 +389,7 @@ public partial class Gambling : GamblingModule<GamblingService>
[Priority(0)]
public Task CurrencyTransactions([Leftover] IUser usr)
=> InternalCurrencyTransactions(usr.Id, 1);
[Cmd]
[OwnerOnly]
[Priority(-1)]
@@ -872,9 +871,6 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else if (result.Result == RpsResultType.Win)
{
if ((long)result.Won > 0)
embed.AddField(GetText(strs.won), N((long)result.Won));
msg = GetText(strs.rps_win(ctx.User.Mention,
GetRpsPick(pick),
GetRpsPick((InputRpsPick)result.ComputerPick)));
@@ -890,6 +886,13 @@ public partial class Gambling : GamblingModule<GamblingService>
.WithOkColor()
.WithDescription(msg);
if (amount > 0)
{
embed
.AddField(GetText(strs.bet), N(amount), true)
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true);
}
await Response().Embed(embed).SendAsync();
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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)

View File

@@ -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();
}
}
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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));

View File

@@ -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))
{

View File

@@ -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)

View File

@@ -14,7 +14,7 @@ public partial class Utility
}
private string GetCommandString(NadekoCommandCallModel res)
=> $"{_bcs.Data.Prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
=> $"{prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
private static string GetParamString(string val, bool isLast)
=> isLast ? val : "\"" + val + "\"";

View File

@@ -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));

View File

@@ -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
{

View File

@@ -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()

View File

@@ -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),

View File

@@ -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)

View File

@@ -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)

View File

@@ -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();

View File

@@ -0,0 +1,11 @@
namespace NadekoBot.Modules.Xp.Services;
public enum BuyResult
{
Success,
XpShopDisabled,
AlreadyOwned,
InsufficientFunds,
UnknownItem,
InsufficientPatronTier,
}

View File

@@ -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);
}
}

View File

@@ -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
});
}
}

View File

@@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.2.0</Version>
<Version>5.3.3</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"/>

View 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>

View File

@@ -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,
}

View File

@@ -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; }

View File

@@ -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);

View File

@@ -19,7 +19,6 @@ public abstract class NadekoModule : ModuleBase
public INadekoInteractionService _inter { get; set; }
public IReplacementService repSvc { get; set; }
public IMessageSenderService _sender { get; set; }
public BotConfigService _bcs { get; set; }
protected string prefix
=> _cmdHandler.GetPrefix(ctx.Guild);
@@ -31,7 +30,7 @@ public abstract class NadekoModule : ModuleBase
=> _sender.CreateEmbed(ctx.Guild?.Id);
public ResponseBuilder Response()
=> new ResponseBuilder(Strings, _bcs, (DiscordSocketClient)ctx.Client)
=> new ResponseBuilder(Strings, _sender, (DiscordSocketClient)ctx.Client)
.Context(ctx);
protected override void BeforeExecute(CommandInfo command)

View File

@@ -9,31 +9,31 @@ public sealed class MessageSenderService : IMessageSenderService, INService
public MessageSenderService(
IBotStrings bs,
BotConfigService bcs,
DiscordSocketClient client,
IGuildColorsService gcs)
IGuildColorsService gcs,
BotConfigService bcs)
{
_bs = bs;
_bcs = bcs;
_client = client;
_gcs = gcs;
_bcs = bcs;
}
public ResponseBuilder Response(IMessageChannel channel)
=> new ResponseBuilder(_bs, _bcs, _client)
=> new ResponseBuilder(_bs, this, _client)
.Channel(channel);
public ResponseBuilder Response(ICommandContext ctx)
=> new ResponseBuilder(_bs, _bcs, _client)
=> new ResponseBuilder(_bs, this, _client)
.Context(ctx);
public ResponseBuilder Response(IUser user)
=> new ResponseBuilder(_bs, _bcs, _client)
=> new ResponseBuilder(_bs, this, _client)
.User(user);
public ResponseBuilder Response(SocketMessageComponent smc)
=> new ResponseBuilder(_bs, _bcs, _client)
=> new ResponseBuilder(_bs, this, _client)
.Channel(smc.Channel);
public NadekoEmbedBuilder CreateEmbed(ulong? guildId = null)
@@ -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()

View File

@@ -6,16 +6,17 @@ public sealed partial class ResponseBuilder
{
private ICommandContext? ctx;
private IMessageChannel? channel;
private IUser? user;
private IUserMessage? msg;
private string? plainText;
private IReadOnlyCollection<EmbedBuilder>? embeds;
private IUserMessage? msg;
private IUser? user;
private bool sanitizeMentions = true;
private LocStr? locTxt;
private object[] locParams = [];
private bool shouldReply = true;
private readonly IBotStrings _bs;
private readonly BotConfigService _bcs;
private readonly IMessageSenderService _sender;
private EmbedBuilder? embedBuilder;
private NadekoInteractionBase? inter;
private Stream? fileStream;
@@ -25,10 +26,10 @@ public sealed partial class ResponseBuilder
public DiscordSocketClient Client { get; set; }
public ResponseBuilder(IBotStrings bs, BotConfigService bcs, DiscordSocketClient client)
public ResponseBuilder(IBotStrings bs, IMessageSenderService sender, DiscordSocketClient client)
{
_bs = bs;
_bcs = bcs;
_sender = sender;
Client = client;
}
@@ -197,7 +198,7 @@ public sealed partial class ResponseBuilder
string? url = null,
string? footer = null)
{
var eb = new NadekoEmbedBuilder(_bcs)
var eb = _sender.CreateEmbed(ctx?.Guild?.Id ?? (channel as ITextChannel)?.GuildId ?? (user as IGuildUser)?.GuildId)
.WithDescription(text);
if (!string.IsNullOrWhiteSpace(title))

View File

@@ -0,0 +1,8 @@
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}

View File

@@ -75,6 +75,14 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN
{
GuildId = guildId
});
if (!_colors.TryAdd(guildId, new Colors(null, null, color?.ToDiscordColor())))
{
_colors[guildId] = _colors[guildId] with
{
Error = color?.ToDiscordColor()
};
}
}
public async Task SetPendingColor(ulong guildId, Rgba32? color)
@@ -95,6 +103,14 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN
{
GuildId = guildId
});
if (!_colors.TryAdd(guildId, new Colors(null, color?.ToDiscordColor(), null)))
{
_colors[guildId] = _colors[guildId] with
{
Warn = color?.ToDiscordColor()
};
}
}
public async Task OnReadyAsync()
@@ -110,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);
}

View File

@@ -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);

View 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;
}
}

View File

@@ -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;
});
}
}

View File

@@ -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(),

View File

@@ -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));
}
}
}

View File

@@ -9,7 +9,7 @@ public static class SocketMessageComponentExtensions
MsgType type,
bool ephemeral = false)
{
var embed = sender.CreateEmbed().WithDescription(text);
var embed = sender.CreateEmbed(ch.GuildId).WithDescription(text);
embed = (type switch
{

View File

@@ -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

View File

@@ -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.

View File

@@ -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"

View File

@@ -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."

View File

@@ -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."
}