Compare commits

..

35 Commits
5.2.2 ... 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
92 changed files with 9986 additions and 848 deletions

View File

@@ -2,6 +2,99 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o 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 ## [5.2.2] - 27.11.2024
### Changed ### Changed
@@ -26,46 +119,49 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Added `.todo undone` command to unmark a todo as done - Added `.todo undone` command to unmark a todo as done
- Added Button Roles! - Added Button Roles!
- `.btr a` to add a button role to the specified message - `.btr a` to add a button role to the specified message
- `.btr list` to list all button roles on the server - `.btr list` to list all button roles on the server
- `.btr rm` to remove a button role from the specified message - `.btr rm` to remove a button role from the specified message
- `.btr rma` to remove all button roles on 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) - `.btr excl` to toggle exclusive button roles (only 1 role per message or any number)
- Use `.h btr` for more info - Use `.h btr` for more info
- Added `.wrongsong` which will delete the last queued song. - Added `.wrongsong` which will delete the last queued song.
- Useful in case you made a mistake, or the bot queued a wrong 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. - It will reset after a shuffle or fairplay toggle, or similar events.
- Added Server color Commands! - 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) - Every Server can now set their own colors for ok/error/pending embed (the default green/red/yellow color on the
- Use `.h .sclr` to see the list of commands left side of the message the bot sends)
- `.sclr show` will show the current server colors - Use `.h .sclr` to see the list of commands
- `.sclr ok <color hex>` to set ok color - `.sclr show` will show the current server colors
- `.sclr warn <color hex>` to set warn color - `.sclr ok <color hex>` to set ok color
- `.sclr error <color hex>` to set error color - `.sclr warn <color hex>` to set warn color
- `.sclr error <color hex>` to set error color
### Changed ### Changed
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands - Self Assigned Roles reworked! Use `.h .sar` for the list of commands
- `.sar autodel` - `.sar autodel`
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands. - Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
- `.sar ad` - `.sar ad`
- Adds a role to the list of self-assignable roles. You can also specify a group. - 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. - If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role
- `.sar groupname` per group.
- Sets a self assignable role group name. Provide no name to remove. - `.sar groupname`
- `.sar remove` - Sets a self assignable role group name. Provide no name to remove.
- Removes a specified role from the list of self-assignable roles. - `.sar remove`
- `.sar list` - Removes a specified role from the list of self-assignable roles.
- Lists self-assignable roles. Shows 20 roles per page. - `.sar list`
- `.sar exclusive` - Lists self-assignable roles. Shows 20 roles per page.
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group. - `.sar exclusive`
- `.sar rolelvlreq` - Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role
- Set a level requirement on a self-assignable role. per group.
- `.sar grouprolereq` - `.sar rolelvlreq`
- Set a role that users have to have in order to assign a self-assignable role from the specified group. - Set a level requirement on a self-assignable role.
- `.sar groupdelete` - `.sar grouprolereq`
- Deletes a self-assignable role group - Set a role that users have to have in order to assign a self-assignable role from the specified group.
- `.iam` and `.iamn` are unchanged - `.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. - Removed patron limits from Reaction Roles. Anyone can have as many reros as they like.
- `.timely` captcha made stronger and cached per user. - `.timely` captcha made stronger and cached per user.
- `.bsreset` price reduced by 90% - `.bsreset` price reduced by 90%
@@ -79,9 +175,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Added ### Added
- Added `.rakeback` command, get a % of house edge back as claimable currency - 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 - 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 - 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 - 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 `.betstatsreset` / `.bsreset` command to reset your stats for a fee
- Added `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games - Added `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games
- Added `.waifuclaims` command which lists all of your claimed waifus - Added `.waifuclaims` command which lists all of your claimed waifus
@@ -91,9 +187,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- `.divorce` no longer has a cooldown - `.divorce` no longer has a cooldown
- `.betroll` has a 2% better payout - `.betroll` has a 2% better payout
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins, - `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
- now has a new symbol, wheat - now has a new symbol, wheat
- worse around 1% in total (now shares the top spot with .bf) - worse around 1% in total (now shares the top spot with .bf)
## [5.1.19] - 04.11.2024 ## [5.1.19] - 04.11.2024
@@ -112,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 - `.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). - `.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 ### Fixed

View File

@@ -1,5 +1,5 @@
# Config # 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` to see the list of editable config files
Use `.config <config-name>` to see the list of settable properties on that config Use `.config <config-name>` to see the list of settable properties on that config

View File

@@ -1,27 +1,30 @@
## Creds Guide ## Creds Guide
This document aims to guide you through the process of creating a Discord account for your bot 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.
(the Discord Bot application), and inviting that account into your Discord server.
- 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) ![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]. 1. Go to [the Discord developer application page][DiscordApp].
- Log in with your Discord account. 2. Log in with your Discord account.
- Click **New Application**. 3. Click **New Application**.
- Fill out the `Name` field however you like. 3. Fill out the `Name` field however you like, accept the terms, and confirm.
- Go to the **Bot** tab on the left sidebar. 1. 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. 1. 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. 1. **Optional:** Add bot's avatar and description.
- Copy your Token to `creds.yml` as shown above. 1. Copy your Token to `creds.yml` as shown above.
- Scroll down to the **`Privileged Gateway Intents`** section 1. Scroll down to the **`Privileged Gateway Intents`** section
- **Enable the following:** - Enable the following:
- **PRESENCE INTENT** - **PRESENCE INTENT**
- **SERVER MEMBERS INTENT** - **SERVER MEMBERS INTENT**
- **MESSAGE CONTENT INTENT** - **MESSAGE CONTENT INTENT**
These are required for a number of features to function properly, and all should be on. 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 - 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)*. *(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: 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 - Access to exclusive **#noticed** text and voice channels
- **1000 flowers** on the public bot per dollar donated (after fees) - **3000 flowers** on the public bot per dollar donated (after fees)
- **Expressions** on the public bot for [Patreon pledges][patreon] of $5 or higher
## Patreon ## 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`. 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 !!! 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] [![img][paypal-button]][paypal]

View File

@@ -1,11 +1,11 @@
## Setting Up NadekoBot on Windows With the Updater ## Setting Up NadekoBot on Windows With the Updater
| Table of Contents| | Table of Contents|
| :---------------------------------------------------------------------------------------------------------------------------| | :-|
| [Prerequisites](#prerequisites) | | [Prerequisites](#prerequisites) |
| [Setup](#setup) | | [Setup](#setup) |
| [Starting the Bot](#starting-the-bot) | | [Starting the Bot](#starting-the-bot) |
| [Updating Nadeko](#updating-nadeko) | | [Updating Nadeko](#updating-nadeko) |
| [Manually Installing the Prerequisites from the Updater](#music-prerequisites) | | [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.* *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 #### Prerequisites
- Windows 10 or later (64-bit) - Windows 10 or later (64-bit)
- [Create a Discord Bot application and invite the bot to your server](../creds-guide.md)
**Optional** **Optional**
@@ -29,8 +28,7 @@
![Create a new bot](https://i.imgur.com/JxtRk9e.png "Create a new bot") ![Create a new bot](https://i.imgur.com/JxtRk9e.png "Create a new bot")
- Click on **`DOWNLOAD`** at the lower right - Click on **`DOWNLOAD`** at the lower right
![Bot Setup](https://i.imgur.com/HqAl36p.png "Bot Setup") ![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 want to use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DLP`**.
- If you will 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). - 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. - 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. - 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. - 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 #### Updating Nadeko
- Make sure Nadeko is closed and not running - Make sure Nadeko is closed and not running
@@ -52,71 +48,6 @@
- Launch the bot - Launch the bot
- You've updated and are running again, easy as that! - 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 #### Music prerequisites
In order to use music commands, you need ffmpeg and yt-dlp installed. 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 [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-32bit]: https://cdn.nadeko.bot/dl/ffmpeg-32.zip
[ffmpeg-64bit]: https://cdn.nadeko.bot/dl/ffmpeg-64.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-microsoft-windows: Windows guide][windows-guide]
- [:material-linux: Linux guide][linux-guide] - [:material-linux: Linux guide][linux-guide]
- [:material-apple: Mac OS guide][macos-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. 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/ [invite]: https://invite.nadeko.bot/
[commands]: https://nadeko.bot/commands/ [commands]: https://nadeko.bot/commands/
[windows-guide]: ./guides/windows-guide.md [windows-guide]: ./guides/windows-guide.md
[windows-source-guide]: ./guides/windows-source-guide.md
[linux-guide]: ./guides/linux-guide.md [linux-guide]: ./guides/linux-guide.md
[macos-guide]: ./guides/osx-guide.md [macos-guide]: ./guides/osx-guide.md
[from-source-guide]: ./guides/from-source.md [from-source-guide]: ./guides/from-source.md

View File

@@ -75,6 +75,7 @@ nav:
- Windows Guide: guides/windows-guide.md - Windows Guide: guides/windows-guide.md
- Linux Guide: guides/linux-guide.md - Linux Guide: guides/linux-guide.md
- OSX Guide: guides/osx-guide.md - OSX Guide: guides/osx-guide.md
- Windows Guide (from source): guides/windows-source-guide.md
- Docker Guide (unsupported): guides/docker-guide.md - Docker Guide (unsupported): guides/docker-guide.md
- Commands: - Commands:
- Readme: commands-readme.md - Readme: commands-readme.md

View File

@@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="Serilog" Version="3.1.1" />
<PackageReference Include="YamlDotNet" Version="15.1.4" /> <PackageReference Include="YamlDotNet" Version="15.1.4" />
</ItemGroup> </ItemGroup>

View File

@@ -357,3 +357,4 @@ resharper_arrange_redundant_parentheses_highlighting = hint
# IDE0011: Add braces # IDE0011: Add braces
dotnet_diagnostic.IDE0011.severity = warning dotnet_diagnostic.IDE0011.severity = warning
resharper_arrange_type_member_modifiers_highlighting = hint

View File

@@ -20,7 +20,6 @@ public static class UserXpExtensions
{ {
Xp = 0, Xp = 0,
UserId = userId, UserId = userId,
NotifyOnLevelUp = XpNotificationLocation.None,
GuildId = guildId 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) public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
=> await xps.ToLinqToDBTable() => await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp) .OrderByDescending(x => x.Xp)
.Take(count) .Take(count)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId) public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> await xps.ToLinqToDBTable() => await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId .Where(x => x.GuildId == guildId
&& x.Xp + x.AwardedXp && x.Xp
> xps.AsQueryable() > xps.AsQueryable()
.Where(y => y.UserId == userId && y.GuildId == guildId) .Where(y => y.UserId == userId && y.GuildId == guildId)
.Select(y => y.Xp + y.AwardedXp) .Select(y => y.Xp)
.FirstOrDefault()) .FirstOrDefault())
.CountAsyncLinqToDB() .CountAsyncLinqToDB()
+ 1; + 1;
@@ -53,6 +52,6 @@ public static class UserXpExtensions
=> await userXp => await userXp
.Where(x => x.GuildId == guildId && x.UserId == userId) .Where(x => x.GuildId == guildId && x.UserId == userId)
.FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs
? new(uxs.Xp + uxs.AwardedXp) ? new(uxs.Xp)
: new(0); : new(0);
} }

View File

@@ -4,38 +4,28 @@ namespace NadekoBot.Db;
public readonly struct LevelStats public readonly struct LevelStats
{ {
public const int XP_REQUIRED_LVL_1 = 36;
public long Level { get; } public long Level { get; }
public long LevelXp { get; } public long LevelXp { get; }
public long RequiredXp { get; } public long RequiredXp { get; }
public long TotalXp { get; } public long TotalXp { get; }
public LevelStats(long xp) public LevelStats(long totalXp)
{ {
if (xp < 0) if (totalXp < 0)
xp = 0; totalXp = 0;
TotalXp = xp; TotalXp = totalXp;
Level = GetLevelByTotalXp(totalXp);
const int baseXp = XP_REQUIRED_LVL_1; LevelXp = totalXp - GetTotalXpReqForLevel(Level);
RequiredXp = (9 * (Level + 1)) + 27;
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;
} }
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<FilterChannelId> FilterInvitesChannelIds { get; set; } = new();
public HashSet<FilterLinksChannelId> FilterLinksChannelIds { 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 bool FilterWords { get; set; }
public HashSet<FilteredWord> FilteredWords { get; set; } = new(); public HashSet<FilteredWord> FilteredWords { get; set; } = new();
public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new(); public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new();
// mute
public HashSet<MutedUserId> MutedUsers { get; set; } = new(); public HashSet<MutedUserId> MutedUsers { get; set; } = new();
public string MuteRoleName { get; set; } public string MuteRoleName { get; set; }
// chatterbot
public bool CleverbotEnabled { get; set; } public bool CleverbotEnabled { get; set; }
// protection
public AntiRaidSetting AntiRaidSetting { get; set; } public AntiRaidSetting AntiRaidSetting { get; set; }
public AntiSpamSetting AntiSpamSetting { get; set; } public AntiSpamSetting AntiSpamSetting { get; set; }
public AntiAltSetting AntiAltSetting { get; set; } public AntiAltSetting AntiAltSetting { get; set; }
// time
public string Locale { get; set; } public string Locale { get; set; }
public string TimeZoneId { get; set; } public string TimeZoneId { get; set; }
// timers
public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new(); public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new();
public HashSet<UnbanTimer> UnbanTimer { get; set; } = new(); public HashSet<UnbanTimer> UnbanTimer { get; set; } = new();
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new(); public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
// vcrole
public HashSet<VcRoleInfo> VcRoleInfos { get; set; } public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
// aliases
public HashSet<CommandAlias> CommandAliases { get; set; } = new(); public HashSet<CommandAlias> CommandAliases { get; set; } = new();
public bool WarningsInitialized { get; set; } public bool WarningsInitialized { get; set; }
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { 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 UserId { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public long Xp { 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 public enum XpNotificationLocation
{ {

View File

@@ -74,6 +74,35 @@ public abstract class NadekoContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) 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 #region GuildColors
modelBuilder.Entity<GuildColors>() modelBuilder.Entity<GuildColors>()
@@ -135,13 +164,18 @@ public abstract class NadekoContext : DbContext
#region UserBetStats #region UserBetStats
modelBuilder.Entity<UserBetStats>() modelBuilder.Entity<UserBetStats>(ubs =>
.HasIndex(x => new {
{ ubs.HasIndex(x => new
x.UserId, {
x.Game x.UserId,
}) x.Game
.IsUnique(); })
.IsUnique();
ubs.HasIndex(x => x.MaxWin)
.IsUnique(false);
});
#endregion #endregion
@@ -449,7 +483,6 @@ public abstract class NadekoContext : DbContext
xps.HasIndex(x => x.UserId); xps.HasIndex(x => x.UserId);
xps.HasIndex(x => x.GuildId); xps.HasIndex(x => x.GuildId);
xps.HasIndex(x => x.Xp); xps.HasIndex(x => x.Xp);
xps.HasIndex(x => x.AwardedXp);
#endregion #endregion

View File

@@ -5,6 +5,16 @@ namespace NadekoBot.Migrations;
public static class MigrationQueries 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) public static void MigrateSar(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.Sql(""" migrationBuilder.Sql("""

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); 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 => modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{ {
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
@@ -2702,6 +2738,47 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("streamrolewhitelisteduser", (string)null); 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 => modelBuilder.Entity("NadekoBot.Db.Models.TodoModel", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -2858,10 +2935,6 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("AwardedXp")
.HasColumnType("bigint")
.HasColumnName("awardedxp");
b.Property<DateTime?>("DateAdded") b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone") .HasColumnType("timestamp without time zone")
.HasColumnName("dateadded"); .HasColumnName("dateadded");
@@ -2870,10 +2943,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("guildid"); .HasColumnName("guildid");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("integer")
.HasColumnName("notifyonlevelup");
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("userid"); .HasColumnName("userid");
@@ -2885,9 +2954,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_userxpstats"); .HasName("pk_userxpstats");
b.HasIndex("AwardedXp")
.HasDatabaseName("ix_userxpstats_awardedxp");
b.HasIndex("GuildId") b.HasIndex("GuildId")
.HasDatabaseName("ix_userxpstats_guildid"); .HasDatabaseName("ix_userxpstats_guildid");
@@ -3416,6 +3482,9 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_userbetstats"); .HasName("pk_userbetstats");
b.HasIndex("MaxWin")
.HasDatabaseName("ix_userbetstats_maxwin");
b.HasIndex("UserId", "Game") b.HasIndex("UserId", "Game")
.IsUnique() .IsUnique()
.HasDatabaseName("ix_userbetstats_userid_game"); .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"); 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 => modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{ {
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
@@ -2013,6 +2040,36 @@ namespace NadekoBot.Migrations
b.ToTable("StreamRoleWhitelistedUser"); 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 => modelBuilder.Entity("NadekoBot.Db.Models.TodoModel", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -2127,18 +2184,12 @@ namespace NadekoBot.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<long>("AwardedXp")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded") b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<ulong>("GuildId") b.Property<ulong>("GuildId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@@ -2147,8 +2198,6 @@ namespace NadekoBot.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AwardedXp");
b.HasIndex("GuildId"); b.HasIndex("GuildId");
b.HasIndex("UserId"); b.HasIndex("UserId");
@@ -2541,6 +2590,8 @@ namespace NadekoBot.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("MaxWin");
b.HasIndex("UserId", "Game") b.HasIndex("UserId", "Game")
.IsUnique(); .IsUnique();

View File

@@ -46,7 +46,7 @@ public partial class Administration : NadekoModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageGuild)] [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); var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
if (newValue) if (newValue)
@@ -59,7 +59,7 @@ public partial class Administration : NadekoModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageGuild)] [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); var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
if (newValue) if (newValue)
@@ -72,10 +72,10 @@ public partial class Administration : NadekoModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageChannels)] [UserPerm(ChannelPerm.ManageChannels)]
[BotPerm(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; var seconds = (int?)timespan?.Time.TotalSeconds ?? 0;
if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6))) if (timespan is not null && (timespan.Time < TimeSpan.FromSeconds(0) || timespan.Time > TimeSpan.FromHours(6)))
return; return;
await ((ITextChannel)ctx.Channel).ModifyAsync(tcp => await ((ITextChannel)ctx.Channel).ModifyAsync(tcp =>
@@ -298,18 +298,18 @@ public partial class Administration : NadekoModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)] [UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)] [BotPerm(ChannelPerm.ManageMessages)]
public Task Delete(ulong messageId, StoopidTime time = null) public Task Delete(ulong messageId, ParsedTimespan timespan = null)
=> Delete((ITextChannel)ctx.Channel, messageId, time); => Delete((ITextChannel)ctx.Channel, messageId, timespan);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null) public async Task Delete(ITextChannel channel, ulong messageId, ParsedTimespan timespan = null)
=> await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync()); => await InternalMessageAction(channel, messageId, timespan, msg => msg.DeleteAsync());
private async Task InternalMessageAction( private async Task InternalMessageAction(
ITextChannel channel, ITextChannel channel,
ulong messageId, ulong messageId,
StoopidTime time, ParsedTimespan timespan,
Func<IMessage, Task> func) Func<IMessage, Task> func)
{ {
var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel); var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
@@ -334,13 +334,13 @@ public partial class Administration : NadekoModule<AdministrationService>
return; return;
} }
if (time is null) if (timespan is null)
await msg.DeleteAsync(); await msg.DeleteAsync();
else if (time.Time <= TimeSpan.FromDays(7)) else if (timespan.Time <= TimeSpan.FromDays(7))
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await Task.Delay(time.Time); await Task.Delay(timespan.Time);
await msg.DeleteAsync(); await msg.DeleteAsync();
}); });
} }

View File

@@ -71,7 +71,7 @@ public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoC
try try
{ {
Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id); 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); await user.Guild.RemoveBanAsync(user.Id);
} }
catch (Exception e) catch (Exception e)

View File

@@ -72,18 +72,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
[Priority(1)] [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; return;
try try
{ {
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return; 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()), await Response().Confirm(strs.user_muted_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes)).SendAsync(); (int)timespan.Time.TotalMinutes)).SendAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -133,18 +133,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[Priority(1)] [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; return;
try try
{ {
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return; 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()), 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) catch (Exception ex)
{ {
@@ -193,18 +193,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.MuteMembers)] [UserPerm(GuildPerm.MuteMembers)]
[Priority(1)] [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; return;
try try
{ {
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return; 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()), await Response().Confirm(strs.user_voice_mute_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes)).SendAsync(); (int)timespan.Time.TotalMinutes)).SendAsync();
} }
catch 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 public partial class Administration
{ {
[Group] [Group]
public partial class PlayingRotateCommands : NadekoModule<PlayingRotateService> public partial class PlayingRotateCommands : NadekoModule<IBotActivityService>
{ {
[Cmd] [Cmd]
[OwnerOnly] [OwnerOnly]

View File

@@ -28,17 +28,17 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt( public async Task AntiAlt(
StoopidTime minAge, ParsedTimespan minAge,
PunishmentAction action, PunishmentAction action,
[Leftover] StoopidTime punishTime = null) [Leftover] ParsedTimespan punishTimespan = null)
{ {
var minAgeMinutes = (int)minAge.Time.TotalMinutes; 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) if (minAgeMinutes < 1 || punishTimeMinutes < 0)
return; return;
var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0; var minutes = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
if (action is PunishmentAction.TimeOut && minutes < 1) if (action is PunishmentAction.TimeOut && minutes < 1)
minutes = 1; minutes = 1;
@@ -53,7 +53,7 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [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; var minAgeMinutes = (int)minAge.Time.TotalMinutes;
@@ -86,8 +86,8 @@ public partial class Administration
int userThreshold, int userThreshold,
int seconds, int seconds,
PunishmentAction action, PunishmentAction action,
[Leftover] StoopidTime punishTime) [Leftover] ParsedTimespan punishTimespan)
=> InternalAntiRaid(userThreshold, seconds, action, punishTime); => InternalAntiRaid(userThreshold, seconds, action, punishTimespan);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
@@ -100,7 +100,7 @@ public partial class Administration
int userThreshold, int userThreshold,
int seconds = 10, int seconds = 10,
PunishmentAction action = PunishmentAction.Mute, PunishmentAction action = PunishmentAction.Mute,
StoopidTime punishTime = null) ParsedTimespan punishTimespan = null)
{ {
if (action == PunishmentAction.AddRole) if (action == PunishmentAction.AddRole)
{ {
@@ -120,13 +120,13 @@ public partial class Administration
return; return;
} }
if (punishTime is not null) if (punishTimespan is not null)
{ {
if (!_service.IsDurationAllowed(action)) if (!_service.IsDurationAllowed(action))
await Response().Error(strs.prot_cant_use_time).SendAsync(); 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) if (time is < 0 or > 60 * 24)
return; return;
@@ -170,8 +170,8 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[Priority(1)] [Priority(1)]
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime) public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] ParsedTimespan punishTimespan)
=> InternalAntiSpam(messageCount, action, punishTime); => InternalAntiSpam(messageCount, action, punishTimespan);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
@@ -183,19 +183,19 @@ public partial class Administration
private async Task InternalAntiSpam( private async Task InternalAntiSpam(
int messageCount, int messageCount,
PunishmentAction action, PunishmentAction action,
StoopidTime timeData = null, ParsedTimespan timespanData = null,
IRole role = null) IRole role = null)
{ {
if (messageCount is < 2 or > 10) if (messageCount is < 2 or > 10)
return; return;
if (timeData is not null) if (timespanData is not null)
{ {
if (!_service.IsDurationAllowed(action)) if (!_service.IsDurationAllowed(action))
await Response().Error(strs.prot_cant_use_time).SendAsync(); 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) if (time is < 0 or > 60 * 24)
return; return;

View File

@@ -22,6 +22,7 @@ public class ProtectionService : INService
private readonly MuteService _mute; private readonly MuteService _mute;
private readonly DbService _db; private readonly DbService _db;
private readonly UserPunishService _punishService; private readonly UserPunishService _punishService;
private readonly INotifySubscriber _notifySub;
private readonly Channel<PunishQueueItem> _punishUserQueue = private readonly Channel<PunishQueueItem> _punishUserQueue =
Channel.CreateUnbounded<PunishQueueItem>(new() Channel.CreateUnbounded<PunishQueueItem>(new()
@@ -35,12 +36,14 @@ public class ProtectionService : INService
IBot bot, IBot bot,
MuteService mute, MuteService mute,
DbService db, DbService db,
UserPunishService punishService) UserPunishService punishService,
INotifySubscriber notifySub)
{ {
_client = client; _client = client;
_mute = mute; _mute = mute;
_db = db; _db = db;
_punishService = punishService; _punishService = punishService;
_notifySub = notifySub;
var ids = client.GetGuildIds(); var ids = client.GetGuildIds();
using (var uow = db.GetDbContext()) using (var uow = db.GetDbContext())
@@ -175,6 +178,9 @@ public class ProtectionService : INService
alts.RoleId, alts.RoleId,
user); user);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
ProtectionType.Alting,
user.Id));
return; return;
} }
} }
@@ -194,6 +200,8 @@ public class ProtectionService : INService
var settings = stats.AntiRaidSettings; var settings = stats.AntiRaidSettings;
await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users); 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); await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
@@ -246,6 +254,10 @@ public class ProtectionService : INService
settings.MuteTime, settings.MuteTime,
settings.RoleId, settings.RoleId,
(IGuildUser)msg.Author); (IGuildUser)msg.Author);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
ProtectionType.Spamming,
msg.Author.Id));
} }
} }
} }

View File

@@ -1,4 +1,6 @@
#nullable disable #nullable disable
using Google.Protobuf.WellKnownTypes;
using NadekoBot.Common.TypeReaders.Models;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
@@ -13,13 +15,18 @@ public partial class Administration
Excl Excl
} }
private readonly TempRoleService _tempRoleService;
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private StickyRolesService _stickyRoleSvc; private StickyRolesService _stickyRoleSvc;
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc) public RoleCommands(
IServiceProvider services,
StickyRolesService stickyRoleSvc,
TempRoleService tempRoleService)
{ {
_services = services; _services = services;
_stickyRoleSvc = stickyRoleSvc; _stickyRoleSvc = stickyRoleSvc;
_tempRoleService = tempRoleService;
} }
[Cmd] [Cmd]
@@ -34,13 +41,16 @@ public partial class Administration
return; return;
try try
{ {
await targetUser.AddRoleAsync(roleToAdd, new RequestOptions() await targetUser.AddRoleAsync(roleToAdd,
{ new RequestOptions()
AuditLogReason = $"Added by [{ctx.User.Username}]" {
}); AuditLogReason = $"Added by [{ctx.User.Username}]"
});
await Response().Confirm(strs.setrole(Format.Bold(roleToAdd.Name), await Response()
Format.Bold(targetUser.ToString()))).SendAsync(); .Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
Format.Bold(targetUser.ToString())))
.SendAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -62,8 +72,10 @@ public partial class Administration
try try
{ {
await targetUser.RemoveRoleAsync(roleToRemove); await targetUser.RemoveRoleAsync(roleToRemove);
await Response().Confirm(strs.remrole(Format.Bold(roleToRemove.Name), await Response()
Format.Bold(targetUser.ToString()))).SendAsync(); .Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
Format.Bold(targetUser.ToString())))
.SendAsync();
} }
catch catch
{ {
@@ -204,5 +216,29 @@ public partial class Administration
await Response().Confirm(strs.sticky_roles_disabled).SendAsync(); 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 Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration.Services;
public sealed class PlayingRotateService : INService, IReadyExecutor public sealed class BotActivityService : IBotActivityService, IReadyExecutor, INService
{ {
private readonly BotConfigService _bss; private readonly TypedKey<ActivityPubData> _activitySetKey = new("activity.set");
private readonly SelfService _selfService;
private readonly IReplacementService _repService; private readonly IPubSub _pubSub;
// private readonly Replacer _rep;
private readonly DbService _db;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IReplacementService _rep;
private readonly BotConfigService _bss;
public PlayingRotateService( public BotActivityService(
IPubSub pubSub,
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
BotConfigService bss, IReplacementService rep,
IEnumerable<IPlaceholderProvider> phProviders, BotConfigService bss)
SelfService selfService,
IReplacementService repService)
{ {
_db = db; _pubSub = pubSub;
_bss = bss;
_selfService = selfService;
_repService = repService;
_client = client; _client = client;
} _db = db;
_rep = rep;
public async Task OnReadyAsync() _bss = bss;
{
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);
}
}
} }
public async Task<string> RemovePlayingAsync(int index) public async Task<string> RemovePlayingAsync(int index)
@@ -116,4 +77,91 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList(); 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 IMedusaLoaderService _medusaLoader;
private readonly ICoordinator _coord; private readonly ICoordinator _coord;
private readonly DbService _db; private readonly DbService _db;
private readonly IBotActivityService _bas;
public SelfCommands( public SelfCommands(
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
IBotStrings strings, IBotStrings strings,
ICoordinator coord, ICoordinator coord,
IMedusaLoaderService medusaLoader) IMedusaLoaderService medusaLoader,
IBotActivityService bas)
{ {
_client = client; _client = client;
_db = db; _db = db;
_strings = strings; _strings = strings;
_coord = coord; _coord = coord;
_medusaLoader = medusaLoader; _medusaLoader = medusaLoader;
_bas = bas;
} }
@@ -63,9 +66,9 @@ public partial class Administration
await message.ModifyAsync(x => await message.ModifyAsync(x =>
x.Embed = CreateEmbed() x.Embed = CreateEmbed()
.WithDescription(GetText(strs.cache_users_done(added, updated))) .WithDescription(GetText(strs.cache_users_done(added, updated)))
.WithOkColor() .WithOkColor()
.Build() .Build()
); );
} }
@@ -116,13 +119,13 @@ public partial class Administration
await Response() await Response()
.Embed(CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.scadd)) .WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server), .AddField(GetText(strs.server),
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}", cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
true) true)
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
.AddField(GetText(strs.command_text), cmdText)) .AddField(GetText(strs.command_text), cmdText))
.SendAsync(); .SendAsync();
} }
@@ -496,7 +499,7 @@ public partial class Administration
// var rep = new ReplacementBuilder().WithDefault(Context).Build(); // var rep = new ReplacementBuilder().WithDefault(Context).Build();
var repCtx = new ReplacementContext(ctx); 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(); await Response().Confirm(strs.set_activity).SendAsync();
} }
@@ -518,7 +521,7 @@ public partial class Administration
{ {
name ??= ""; name ??= "";
await _service.SetStreamAsync(name, url); await _bas.SetStreamAsync(name, url);
await Response().Confirm(strs.set_stream).SendAsync(); await Response().Confirm(strs.set_stream).SendAsync();
} }

View File

@@ -28,7 +28,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
//keys //keys
private readonly TypedKey<ActivityPubData> _activitySetKey;
private readonly TypedKey<string> _guildLeaveKey; private readonly TypedKey<string> _guildLeaveKey;
public SelfService( public SelfService(
@@ -51,11 +50,8 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
_bss = bss; _bss = bss;
_pubSub = pubSub; _pubSub = pubSub;
_sender = sender; _sender = sender;
_activitySetKey = new("activity.set");
_guildLeaveKey = new("guild.leave"); _guildLeaveKey = new("guild.leave");
HandleStatusChanges();
_pubSub.Sub(_guildLeaveKey, _pubSub.Sub(_guildLeaveKey,
async input => async input =>
{ {
@@ -394,49 +390,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
return channelId is not null; 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> /// <summary>
/// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name /// 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. /// 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 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; IUserMessage msg = null;
try try
@@ -84,13 +84,13 @@ public partial class Administration
IUserMessage msg = null; IUserMessage msg = null;
try 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(); msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
return; 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)) if (group is null || group.Roles.All(x => x.RoleId != role.Id))
{ {

View File

@@ -313,7 +313,7 @@ public partial class Administration
int number, int number,
AddRole _, AddRole _,
IRole role, IRole role,
StoopidTime time = null) ParsedTimespan timespan = null)
{ {
var punish = PunishmentAction.AddRole; var punish = PunishmentAction.AddRole;
@@ -324,12 +324,12 @@ public partial class Administration
return; 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) if (!success)
return; return;
if (time is null) if (timespan is null)
{ {
await Response() await Response()
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
@@ -341,7 +341,7 @@ public partial class Administration
await Response() await Response()
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()), Format.Bold(number.ToString()),
Format.Bold(time.Input))) Format.Bold(timespan.Input)))
.SendAsync(); .SendAsync();
} }
} }
@@ -349,7 +349,7 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [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 // this should never happen. Addrole has its own method with higher priority
// also disallow warn punishment for getting warned // also disallow warn punishment for getting warned
@@ -357,15 +357,15 @@ public partial class Administration
return; return;
// you must specify the time for timeout // you must specify the time for timeout
if (punish is PunishmentAction.TimeOut && time is null) if (punish is PunishmentAction.TimeOut && timespan is null)
return; 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) if (!success)
return; return;
if (time is null) if (timespan is null)
{ {
await Response() await Response()
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
@@ -377,7 +377,7 @@ public partial class Administration
await Response() await Response()
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()), Format.Bold(number.ToString()),
Format.Bold(time.Input))) Format.Bold(timespan.Input)))
.SendAsync(); .SendAsync();
} }
} }
@@ -417,17 +417,17 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)] [BotPerm(GuildPerm.BanMembers)]
[Priority(1)] [Priority(1)]
public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null) public Task Ban(ParsedTimespan timespan, IUser user, [Leftover] string msg = null)
=> Ban(time, user.Id, msg); => Ban(timespan, user.Id, msg);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)] [BotPerm(GuildPerm.BanMembers)]
[Priority(0)] [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; return;
var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); 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 defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
var smartText = 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) if (smartText is not null)
await Response().User(guildUser).Text(smartText).SendAsync(); await Response().User(guildUser).Text(smartText).SendAsync();
} }
@@ -456,14 +456,14 @@ public partial class Administration
var user = await ctx.Client.GetUserAsync(userId); var user = await ctx.Client.GetUserAsync(userId);
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; 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() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user)) .WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true) .AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
.AddField("ID", userId.ToString(), true) .AddField("ID", userId.ToString(), true)
.AddField(GetText(strs.duration), .AddField(GetText(strs.duration),
time.Time.ToPrettyStringHm(), timespan.Time.ToPrettyStringHm(),
true); true);
if (dmFailed) if (dmFailed)
@@ -601,7 +601,7 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)] [BotPerm(GuildPerm.BanMembers)]
[Priority(1)] [Priority(1)]
public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null) public Task BanMessageTest(ParsedTimespan duration, [Leftover] string reason = null)
=> InternalBanMessageTest(reason, duration.Time); => InternalBanMessageTest(reason, duration.Time);
private async Task InternalBanMessageTest(string reason, TimeSpan? duration) private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
@@ -790,7 +790,7 @@ public partial class Administration
[UserPerm(GuildPerm.ModerateMembers)] [UserPerm(GuildPerm.ModerateMembers)]
[BotPerm(GuildPerm.ModerateMembers)] [BotPerm(GuildPerm.ModerateMembers)]
[Priority(2)] [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); var user = await ctx.Guild.GetUserAsync(globalUser.Id);
@@ -816,7 +816,7 @@ public partial class Administration
dmFailed = true; dmFailed = true;
} }
await user.SetTimeOutAsync(time.Time); await user.SetTimeOutAsync(timespan.Time);
var toSend = CreateEmbed() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()

View File

@@ -1,6 +1,7 @@
#nullable disable #nullable disable
using NadekoBot.Modules.Gambling.Common; using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Xp.Services;
namespace NadekoBot.Modules.Gambling; namespace NadekoBot.Modules.Gambling;
@@ -10,13 +11,19 @@ public partial class Gambling
public sealed class BetStatsCommands : GamblingModule<UserBetStatsService> public sealed class BetStatsCommands : GamblingModule<UserBetStatsService>
{ {
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IBotCache _cache;
private readonly IUserService _userService;
public BetStatsCommands( public BetStatsCommands(
GamblingTxTracker gamblingTxTracker, GamblingTxTracker gamblingTxTracker,
GamblingConfigService gcs) GamblingConfigService gcs,
IBotCache cache,
IUserService userService)
: base(gcs) : base(gcs)
{ {
_gamblingTxTracker = gamblingTxTracker; _gamblingTxTracker = gamblingTxTracker;
_cache = cache;
_userService = userService;
} }
[Cmd] [Cmd]
@@ -25,12 +32,12 @@ public partial class Gambling
var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game); var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game);
var result = await PromptUserConfirmAsync(CreateEmbed() var result = await PromptUserConfirmAsync(CreateEmbed()
.WithDescription( .WithDescription(
$""" $"""
Are you sure you want to reset your bet stats for **{GetGameName(game)}**? 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) if (!result)
return; return;
@@ -88,15 +95,15 @@ public partial class Gambling
}; };
var eb = CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(user) .WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true) .AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true) .AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true) .AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true) .AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout", .AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture), (stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true); true);
if (game == null) if (game == null)
{ {
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount); var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
@@ -115,13 +122,86 @@ public partial class Gambling
.SendAsync(); .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] [Cmd]
public async Task GambleStats() public async Task GambleStats()
{ {
var stats = await _gamblingTxTracker.GetAllAsync(); var stats = await _gamblingTxTracker.GetAllAsync();
var eb = CreateEmbed() var eb = CreateEmbed()
.WithOkColor(); .WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n"; var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n"; str += "――――――――――――――――――――\n";
@@ -157,13 +237,13 @@ public partial class Gambling
public async Task GambleStatsReset() public async Task GambleStatsReset()
{ {
if (!await PromptUserConfirmAsync(CreateEmbed() if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription( .WithDescription(
""" """
Are you sure? Are you sure?
This will completely reset Gambling Stats. This will completely reset Gambling Stats.
This action is irreversible. This action is irreversible.
"""))) """)))
return; return;
await GambleStats(); await GambleStats();

View File

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

View File

@@ -42,7 +42,7 @@ public sealed class UserBetStatsService : INService
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserBetStats>() await ctx.GetTable<UserBetStats>()
.DeleteAsync(x => x.UserId == userId && (game == null || x.Game == game)); .DeleteAsync(x => x.UserId == userId && (game == null || x.Game == game));
return true; return true;
} }
@@ -52,4 +52,16 @@ public sealed class UserBetStatsService : INService
await ctx.GetTable<GamblingStats>() await ctx.GetTable<GamblingStats>()
.DeleteAsync(); .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 #nullable disable
using NadekoBot.Modules.Games.Services; using NadekoBot.Modules.Games.Services;
using System.Text;
namespace NadekoBot.Modules.Games; namespace NadekoBot.Modules.Games;
@@ -38,10 +39,72 @@ public partial class Games : NadekoModule<GamesService>
return; return;
var res = _service.GetEightballResponse(ctx.User.Id, question); var res = _service.GetEightballResponse(ctx.User.Id, question);
await Response().Embed(CreateEmbed() await Response()
.WithOkColor() .Embed(CreateEmbed()
.WithDescription(ctx.User.ToString()) .WithOkColor()
.AddField("❓ " + GetText(strs.question), question) .WithDescription(ctx.User.ToString())
.AddField("🎱 " + GetText(strs._8ball), res)).SendAsync(); .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); _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(); _thread.Start();
} }
@@ -402,12 +412,24 @@ public sealed class MusicPlayer : IMusicPlayer
if (song is null) if (song is null)
return default; return default;
int index;
if (asNext) var wasLast = _queue.IsLast();
return (_queue.EnqueueNext(song, queuer, out index), index);
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) 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(); 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 settings = _bss.Data;
var commandName = command.Name.ToLowerInvariant(); var commandName = command.Name.ToLowerInvariant();
if (commandName != "resetglobalperms" if (commandName != "resetglobalperms")
&& (settings.Blocked.Commands.Contains(commandName) {
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))) if (settings.Blocked.Commands.Contains(commandName)
return Task.FromResult(true); || 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); return Task.FromResult(false);
} }
@@ -37,13 +46,30 @@ public class GlobalPermissionService : IExecPreCommand, INService
/// </summary> /// </summary>
/// <param name="moduleName">Lowercase module name</param> /// <param name="moduleName">Lowercase module name</param>
/// <returns>Whether the module is added</returns> /// <returns>Whether the module is added</returns>
public bool ToggleModule(string moduleName) public bool ToggleModule(string moduleName, bool priv = false)
{ {
var added = false; var added = false;
_bss.ModifyConfig(bs => _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)) if (bs.Blocked.Modules.Add(moduleName))
{
added = true; added = true;
}
else else
{ {
bs.Blocked.Modules.Remove(moduleName); bs.Blocked.Modules.Remove(moduleName);
@@ -59,13 +85,30 @@ public class GlobalPermissionService : IExecPreCommand, INService
/// </summary> /// </summary>
/// <param name="commandName">Lowercase command name</param> /// <param name="commandName">Lowercase command name</param>
/// <returns>Whether the command is added</returns> /// <returns>Whether the command is added</returns>
public bool ToggleCommand(string commandName) public bool ToggleCommand(string commandName, bool priv = false)
{ {
var added = false; var added = false;
_bss.ModifyConfig(bs => _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)) if (bs.Blocked.Commands.Add(commandName))
{
added = true; added = true;
}
else else
{ {
bs.Blocked.Commands.Remove(commandName); bs.Blocked.Commands.Remove(commandName);

View File

@@ -103,11 +103,11 @@ public partial class Searches : NadekoModule<SearchesService>
} }
var eb = CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.time_new)) .WithTitle(GetText(strs.time_new))
.WithDescription(Format.Code(data.Time.ToString(Culture))) .WithDescription(Format.Code(data.Time.ToString(Culture)))
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
.AddField(GetText(strs.timezone), data.TimeZoneName, true); .AddField(GetText(strs.timezone), data.TimeZoneName, true);
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();
} }
@@ -129,16 +129,16 @@ public partial class Searches : NadekoModule<SearchesService>
await Response() await Response()
.Embed(CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(movie.Title) .WithTitle(movie.Title)
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
.WithDescription(movie.Plot.TrimTo(1000)) .WithDescription(movie.Plot.TrimTo(1000))
.AddField("Rating", movie.ImdbRating, true) .AddField("Rating", movie.ImdbRating, true)
.AddField("Genre", movie.Genre, true) .AddField("Genre", movie.Genre, true)
.AddField("Year", movie.Year, true) .AddField("Year", movie.Year, true)
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
? movie.Poster ? movie.Poster
: null)) : null))
.SendAsync(); .SendAsync();
} }
@@ -191,9 +191,9 @@ public partial class Searches : NadekoModule<SearchesService>
await Response() await Response()
.Embed(CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.AddField(GetText(strs.original_url), $"<{query}>") .AddField(GetText(strs.original_url), $"<{query}>")
.AddField(GetText(strs.short_url), $"<{shortLink}>")) .AddField(GetText(strs.short_url), $"<{shortLink}>"))
.SendAsync(); .SendAsync();
} }
@@ -214,13 +214,13 @@ public partial class Searches : NadekoModule<SearchesService>
} }
var embed = CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(card.Name) .WithTitle(card.Name)
.WithDescription(card.Description) .WithDescription(card.Description)
.WithImageUrl(card.ImageUrl) .WithImageUrl(card.ImageUrl)
.AddField(GetText(strs.store_url), card.StoreUrl, true) .AddField(GetText(strs.store_url), card.StoreUrl, true)
.AddField(GetText(strs.cost), card.ManaCost, true) .AddField(GetText(strs.cost), card.ManaCost, true)
.AddField(GetText(strs.types), card.Types, true); .AddField(GetText(strs.types), card.Types, true);
await Response().Embed(embed).SendAsync(); await Response().Embed(embed).SendAsync();
} }
@@ -281,10 +281,10 @@ public partial class Searches : NadekoModule<SearchesService>
{ {
var item = items[0]; var item = items[0];
return CreateEmbed() return CreateEmbed()
.WithOkColor() .WithOkColor()
.WithUrl(item.Permalink) .WithUrl(item.Permalink)
.WithTitle(item.Word) .WithTitle(item.Word)
.WithDescription(item.Definition); .WithDescription(item.Definition);
}) })
.SendAsync(); .SendAsync();
} }
@@ -312,11 +312,11 @@ public partial class Searches : NadekoModule<SearchesService>
{ {
var model = items.First(); var model = items.First();
var embed = CreateEmbed() var embed = CreateEmbed()
.WithDescription(ctx.User.Mention) .WithDescription(ctx.User.Mention)
.AddField(GetText(strs.word), model.Word, true) .AddField(GetText(strs.word), model.Word, true)
.AddField(GetText(strs._class), model.WordType, true) .AddField(GetText(strs._class), model.WordType, true)
.AddField(GetText(strs.definition), model.Definition) .AddField(GetText(strs.definition), model.Definition)
.WithOkColor(); .WithOkColor();
if (!string.IsNullOrWhiteSpace(model.Example)) if (!string.IsNullOrWhiteSpace(model.Example))
embed.AddField(GetText(strs.example), model.Example); embed.AddField(GetText(strs.example), model.Example);
@@ -404,10 +404,38 @@ public partial class Searches : NadekoModule<SearchesService>
await Response() await Response()
.Embed( .Embed(
CreateEmbed() CreateEmbed()
.WithOkColor() .WithOkColor()
.AddField("Username", usr.ToString()) .AddField("Username", usr.ToString())
.AddField("Avatar Url", avatarUrl) .AddField("Avatar Url", avatarUrl)
.WithThumbnailUrl(avatarUrl.ToString())) .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(); .SendAsync();
} }

View File

@@ -122,12 +122,15 @@ public sealed partial class FlagTranslateService : IReadyExecutor, INService
if (!_supportedFlags.TryGetValue(code, out var lang)) if (!_supportedFlags.TryGetValue(code, out var lang))
return; return;
if (!_msgLangs.Add((reaction.MessageId, lang))) if (_msgLangs.Contains((reaction.MessageId, lang)))
return; return;
var result = await _cache.GetAsync(CdKey(reaction.UserId)); var result = await _cache.GetAsync(CdKey(reaction.UserId));
if (result.TryPickT0(out _, out _)) if (result.TryPickT0(out _, out _))
return; return;
if (!_msgLangs.Add((reaction.MessageId, lang)))
return;
await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5)); await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5));

View File

@@ -28,9 +28,19 @@ public partial class Searches
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
var translation = await _service.Translate(fromLang, toLang, text); 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 catch
{ {
@@ -65,7 +75,10 @@ public partial class Searches
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task AutoTransLang(string fromLang, string toLang) 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) if (succ is null)
{ {
@@ -87,10 +100,10 @@ public partial class Searches
public async Task Translangs() public async Task Translangs()
{ {
var langs = _service.GetLanguages().ToList(); var langs = _service.GetLanguages().ToList();
var eb = CreateEmbed() var eb = CreateEmbed()
.WithTitle(GetText(strs.supported_languages)) .WithTitle(GetText(strs.supported_languages))
.WithOkColor(); .WithOkColor();
foreach (var chunk in langs.Chunk(15)) foreach (var chunk in langs.Chunk(15))
{ {

View File

@@ -9,6 +9,7 @@ public sealed class AfkService : INService, IReadyExecutor
private readonly MessageSenderService _mss; private readonly MessageSenderService _mss;
private static readonly TimeSpan _maxAfkDuration = 8.Hours(); private static readonly TimeSpan _maxAfkDuration = 8.Hours();
public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss) public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss)
{ {
_cache = cache; _cache = cache;
@@ -19,6 +20,9 @@ public sealed class AfkService : INService, IReadyExecutor
private static TypedKey<string> GetKey(ulong userId) private static TypedKey<string> GetKey(ulong userId)
=> new($"afk:msg:{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) public async Task<bool> SetAfkAsync(ulong userId, string text)
{ {
var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true); var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true);
@@ -43,9 +47,7 @@ public sealed class AfkService : INService, IReadyExecutor
msg.DeleteAfter(5); msg.DeleteAfter(5);
}); });
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -61,7 +63,7 @@ public sealed class AfkService : INService, IReadyExecutor
await Task.Delay(_maxAfkDuration); await Task.Delay(_maxAfkDuration);
_client.MessageReceived -= StopAfk; _client.MessageReceived -= StopAfk;
}); });
return added; return added;
} }
@@ -72,36 +74,29 @@ public sealed class AfkService : INService, IReadyExecutor
return Task.CompletedTask; 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; 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; 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; return Task.CompletedTask;
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
var botUser = await tc.Guild.GetCurrentUserAsync();
var perms = botUser.GetPermissions(tc);
if (!perms.SendMessages)
return;
ulong mentionedUserId = 0; ulong mentionedUserId = 0;
if (arg.MentionedUsers.Count <= 3) if (sm.MentionedUsers.Count <= 3)
{ {
foreach (var uid in uMsg.MentionedUserIds) foreach (var uid in uMsg.MentionedUserIds)
{ {
if (uid == arg.Author.Id) if (uid == sm.Author.Id)
continue; continue;
if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>")) if (sm.Content.StartsWith($"<@{uid}>") || sm.Content.StartsWith($"<@!{uid}>"))
{ {
mentionedUserId = uid; mentionedUserId = uid;
break; break;
@@ -115,26 +110,46 @@ public sealed class AfkService : INService, IReadyExecutor
{ {
return; return;
} }
mentionedUserId = repliedUserId; mentionedUserId = repliedUserId;
} }
try try
{ {
var result = await _cache.GetAsync(GetKey(mentionedUserId)); var result = await _cache.GetAsync(GetKey(mentionedUserId));
if (result.TryPickT0(out var msg, out _)) if (result.TryPickT0(out var msg, out _))
{ {
var st = SmartText.CreateFrom(msg); var st = SmartText.CreateFrom(msg);
st = $"The user you've pinged (<#{mentionedUserId}>) is AFK: " + st; st = $"The user you've pinged (<#{mentionedUserId}>) is AFK: " + st;
var toDelete = await _mss.Response(arg.Channel) var toDelete = await _mss.Response(sm.Channel)
.User(arg.Author) .User(sm.Author)
.Message(uMsg) .Message(uMsg)
.Text(st) .Text(st)
.SendAsync(); .SendAsync();
toDelete.DeleteAfter(30); 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) catch (HttpException ex)

View File

@@ -14,7 +14,7 @@ public partial class Utility
} }
private string GetCommandString(NadekoCommandCallModel res) 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) private static string GetParamString(string val, bool isLast)
=> isLast ? val : "\"" + val + "\""; => isLast ? val : "\"" + val + "\"";

View File

@@ -38,6 +38,8 @@ public partial class Utility
eb eb
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.giveaway_started)) .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()}"); .WithFooter($"id: {new kwum(id).ToString()}");
await startingMsg.AddReactionAsync(new Emoji(GiveawayService.GiveawayEmoji)); await startingMsg.AddReactionAsync(new Emoji(GiveawayService.GiveawayEmoji));

View File

@@ -341,6 +341,9 @@ public sealed class GiveawayService : INService, IReadyExecutor
try try
{ {
await msg.ModifyAsync(x => x.Embed = eb.Build()); await msg.ModifyAsync(x => x.Embed = eb.Build());
if (winner is not null)
await _sender.Response(ch).Message(msg).Text($"🎉 <@{winner.UserId}>").SendAsync();
} }
catch catch
{ {

View File

@@ -12,17 +12,21 @@ public partial class Utility
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task ServerColorsShow() 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 = EmbedBuilder[] ebs =
[ [
CreateEmbed() CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription("\\✅"), .WithDescription($"\\✅ {okHex}"),
CreateEmbed() CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithDescription("\\⏳\\⚠️"), .WithDescription($"\\⏳\\⚠️ {warnHex}"),
CreateEmbed() CreateEmbed()
.WithErrorColor() .WithErrorColor()
.WithDescription("\\❌") .WithDescription($"\\❌ {errHex}")
]; ];
await Response() await Response()

View File

@@ -98,10 +98,10 @@ public partial class Utility
return; return;
var embed = CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(guildId is not null .WithTitle(GetText(guildId is not null
? strs.reminder_server_list ? strs.reminder_server_list
: strs.reminder_list)); : strs.reminder_list));
List<Reminder> rems; List<Reminder> rems;
if (guildId is { } gid) if (guildId is { } gid)
@@ -193,23 +193,14 @@ public partial class Utility
message = message.SanitizeAllMentions(); message = message.SanitizeAllMentions();
} }
var rem = new Reminder await _service.AddReminderAsync(ctx.User.Id,
{ targetId,
ChannelId = targetId, ctx.Guild?.Id,
IsPrivate = isPrivate, isPrivate,
When = time, time,
Message = message, message,
UserId = ctx.User.Id, ReminderType.User);
ServerId = ctx.Guild?.Id ?? 0
};
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() await Response()
.Confirm($"\u23f0 {GetText(strs.remind2( .Confirm($"\u23f0 {GetText(strs.remind2(
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username), 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 IMessageSenderService _sender;
private readonly CultureInfo _culture; private readonly CultureInfo _culture;
private TaskCompletionSource<bool> _tcs;
public RemindService( public RemindService(
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
@@ -44,8 +46,7 @@ public class RemindService : INService, IReadyExecutor, IRemindService
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); while (true)
while (await timer.WaitForNextTickAsync())
{ {
await OnReminderLoopTickInternalAsync(); await OnReminderLoopTickInternalAsync();
} }
@@ -55,8 +56,7 @@ public class RemindService : INService, IReadyExecutor, IRemindService
{ {
try try
{ {
var now = DateTime.UtcNow; var reminders = await GetRemindersBeforeAsync();
var reminders = await GetRemindersBeforeAsync(now);
if (reminders.Count == 0) if (reminders.Count == 0)
return; return;
@@ -67,7 +67,6 @@ public class RemindService : INService, IReadyExecutor, IRemindService
{ {
var executedReminders = group.ToList(); var executedReminders = group.ToList();
await executedReminders.Select(ReminderTimerAction).WhenAll(); await executedReminders.Select(ReminderTimerAction).WhenAll();
await RemoveReminders(executedReminders.Select(x => x.Id));
await Task.Delay(1500); await Task.Delay(1500);
} }
} }
@@ -80,21 +79,51 @@ public class RemindService : INService, IReadyExecutor, IRemindService
private async Task RemoveReminders(IEnumerable<int> reminders) private async Task RemoveReminders(IEnumerable<int> reminders)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.Set<Reminder>() await uow.GetTable<Reminder>()
.ToLinqToDBTable()
.DeleteAsync(x => reminders.Contains(x.Id)); .DeleteAsync(x => reminders.Contains(x.Id));
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
private async Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now) private async Task<IReadOnlyList<Reminder>> GetRemindersBeforeAsync()
{ {
await using var uow = _db.GetDbContext(); while (true)
return await uow.Set<Reminder>() {
.ToLinqToDBTable() _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId, _creds.TotalShards, _client.ShardId) await using var uow = _db.GetDbContext();
&& x.When < now) var earliest = await uow.Set<Reminder>()
.ToListAsyncLinqToDB(); .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) public bool TryParseRemindMessage(string input, out RemindObject obj)
@@ -243,21 +272,24 @@ public class RemindService : INService, IReadyExecutor, IRemindService
string message, string message,
ReminderType reminderType) ReminderType reminderType)
{ {
var rem = new Reminder await using (var ctx = _db.GetDbContext())
{ {
UserId = userId, await ctx.GetTable<Reminder>()
ChannelId = targetId, .InsertAsync(() => new Reminder
ServerId = guildId ?? 0, {
IsPrivate = isPrivate, UserId = userId,
When = time, ChannelId = targetId,
Message = message, ServerId = guildId ?? 0,
Type = reminderType IsPrivate = isPrivate,
}; When = time,
Message = message,
Type = reminderType,
DateAdded = DateTime.UtcNow
});
await ctx.SaveChangesAsync();
}
await using var ctx = _db.GetDbContext(); _tcs.SetResult(true);
await ctx.Set<Reminder>()
.AddAsync(rem);
await ctx.SaveChangesAsync();
} }
public async Task<List<Reminder>> GetServerReminders(int page, ulong guildId) public async Task<List<Reminder>> GetServerReminders(int page, ulong guildId)

View File

@@ -110,14 +110,14 @@ public partial class Utility
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(0)] [Priority(0)]
public Task Repeat(StoopidTime interval, [Leftover] string message) public Task Repeat(ParsedTimespan interval, [Leftover] string message)
=> Repeat(ctx.Channel, null, interval, message); => Repeat(ctx.Channel, null, interval, message);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(0)] [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); => Repeat(channel, null, interval, message);
[Cmd] [Cmd]
@@ -138,14 +138,14 @@ public partial class Utility
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(2)] [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); => Repeat(ctx.Channel, timeOfDay, interval, message);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[Priority(3)] [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) [Leftover] string message)
{ {
if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id) if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id)

View File

@@ -186,7 +186,7 @@ public partial class Utility : NadekoModule
return CreateEmbed() return CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.inrole_list(roleName, roleUsers.Count))) .WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
.WithDescription(string.Join("\n", pageUsers)); .WithDescription(string.Join("\n", pageUsers));
}) })
.SendAsync(); .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] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
@@ -154,9 +127,9 @@ public partial class Xp : NadekoModule<XpService>
.Page((items, _) => .Page((items, _) =>
{ {
var embed = CreateEmbed() var embed = CreateEmbed()
.WithTitle(GetText(strs.exclusion_list)) .WithTitle(GetText(strs.exclusion_list))
.WithDescription(string.Join('\n', items)) .WithDescription(string.Join('\n', items))
.WithOkColor(); .WithOkColor();
return embed; return embed;
}) })
@@ -214,16 +187,12 @@ public partial class Xp : NadekoModule<XpService>
for (var i = 0; i < users.Count; i++) 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 user = ((SocketGuild)ctx.Guild).GetUser(users[i].UserId);
var userXpData = users[i]; var userXpData = users[i];
var awardStr = string.Empty; 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()}", embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}",
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}"); $"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
@@ -266,8 +235,8 @@ public partial class Xp : NadekoModule<XpService>
.Page((users, curPage) => .Page((users, curPage) =>
{ {
var embed = CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.global_leaderboard)); .WithTitle(GetText(strs.global_leaderboard));
if (!users.Any()) if (!users.Any())
{ {
@@ -287,6 +256,28 @@ public partial class Xp : NadekoModule<XpService>
.SendAsync(); .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] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
@@ -351,8 +342,8 @@ public partial class Xp : NadekoModule<XpService>
public async Task XpReset(ulong userId) public async Task XpReset(ulong userId)
{ {
var embed = CreateEmbed() var embed = CreateEmbed()
.WithTitle(GetText(strs.reset)) .WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_user_confirm)); .WithDescription(GetText(strs.reset_user_confirm));
if (!await PromptUserConfirmAsync(embed)) if (!await PromptUserConfirmAsync(embed))
return; return;
@@ -368,8 +359,8 @@ public partial class Xp : NadekoModule<XpService>
public async Task XpReset() public async Task XpReset()
{ {
var embed = CreateEmbed() var embed = CreateEmbed()
.WithTitle(GetText(strs.reset)) .WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_server_confirm)); .WithDescription(GetText(strs.reset_server_confirm));
if (!await PromptUserConfirmAsync(embed)) if (!await PromptUserConfirmAsync(embed))
return; return;
@@ -446,20 +437,20 @@ public partial class Xp : NadekoModule<XpService>
{ {
if (!items.Any()) if (!items.Any())
return CreateEmbed() return CreateEmbed()
.WithDescription(GetText(strs.not_found)) .WithDescription(GetText(strs.not_found))
.WithErrorColor(); .WithErrorColor();
var (key, item) = items.FirstOrDefault(); var (key, item) = items.FirstOrDefault();
var eb = CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(item.Name) .WithTitle(item.Name)
.AddField(GetText(strs.price), .AddField(GetText(strs.price),
CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()), CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()),
true) true)
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview) .WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
? item.Url ? item.Url
: item.Preview); : item.Preview);
if (!string.IsNullOrWhiteSpace(item.Desc)) if (!string.IsNullOrWhiteSpace(item.Desc))
eb.AddField(GetText(strs.desc), 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); 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 System.Threading.Channels;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools; using LinqToDB.Tools;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Patronage; using NadekoBot.Modules.Patronage;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception; using Exception = System.Exception;
@@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Xp.Services; 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 public class XpService : INService, IReadyExecutor, IExecNoCommand
{ {
private readonly DbService _db; private readonly DbService _db;
@@ -72,6 +48,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50); private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>(); private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly INotifySubscriber _notifySub;
public XpService( public XpService(
DiscordSocketClient client, DiscordSocketClient client,
@@ -87,7 +64,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
XpConfigService xpConfig, XpConfigService xpConfig,
IPubSub pubSub, IPubSub pubSub,
IPatronageService ps, IPatronageService ps,
IMessageSenderService sender) IMessageSenderService sender,
INotifySubscriber notifySub)
{ {
_db = db; _db = db;
_images = images; _images = images;
@@ -99,6 +77,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
_xpConfig = xpConfig; _xpConfig = xpConfig;
_pubSub = pubSub; _pubSub = pubSub;
_sender = sender; _sender = sender;
_notifySub = notifySub;
_excludedServers = new(); _excludedServers = new();
_excludedChannels = new(); _excludedChannels = new();
_client = client; _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() private async Task UpdateXp()
{ {
try try
@@ -197,9 +168,9 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
var dus = new List<DiscordUser>(globalToAdd.Count); var dus = new List<DiscordUser>(globalToAdd.Count);
var gxps = new List<UserXpStats>(globalToAdd.Count); var gxps = new List<UserXpStats>(globalToAdd.Count);
var conf = _xpConfig.Data;
await using (var ctx = _db.GetDbContext()) await using (var ctx = _db.GetDbContext())
{ {
var conf = _xpConfig.Data;
if (conf.CurrencyPerXp > 0) if (conf.CurrencyPerXp > 0)
{ {
foreach (var user in globalToAdd) foreach (var user in globalToAdd)
@@ -261,8 +232,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
GuildId = guildId, GuildId = guildId,
Xp = group.Key, Xp = group.Key,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
AwardedXp = 0,
NotifyOnLevelUp = XpNotificationLocation.None
}, },
_ => new() _ => new()
{ {
@@ -300,8 +269,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
du.UserId, du.UserId,
false, false,
oldLevel.Level, oldLevel.Level,
newLevel.Level, newLevel.Level));
du.NotifyOnLevelUp));
} }
} }
@@ -310,8 +278,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
if (guildToAdd.TryGetValue(du.GuildId, out var users) if (guildToAdd.TryGetValue(du.GuildId, out var users)
&& users.TryGetValue(du.UserId, out var xpGainData)) && users.TryGetValue(du.UserId, out var xpGainData))
{ {
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount + du.AwardedXp); var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount);
var newLevel = new LevelStats(du.Xp + du.AwardedXp); var newLevel = new LevelStats(du.Xp);
if (oldLevel.Level < newLevel.Level) if (oldLevel.Level < newLevel.Level)
{ {
@@ -321,8 +289,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
du.UserId, du.UserId,
true, true,
oldLevel.Level, oldLevel.Level,
newLevel.Level, newLevel.Level));
du.NotifyOnLevelUp));
} }
} }
} }
@@ -339,8 +306,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
ulong userId, ulong userId,
bool isServer, bool isServer,
long oldLevel, long oldLevel,
long newLevel, long newLevel)
XpNotificationLocation notifyLoc)
=> async () => => async () =>
{ {
if (isServer) if (isServer)
@@ -348,7 +314,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel); 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( private async Task HandleRewardsInternalAsync(
@@ -378,9 +344,45 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
if (role is not null && user is not null) if (role is not null && user is not null)
{ {
if (rrew.Remove) 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 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 channelId,
ulong userId, ulong userId,
bool isServer, bool isServer,
long newLevel, long newLevel)
XpNotificationLocation notifyLoc)
{ {
if (notifyLoc == XpNotificationLocation.None)
return;
var guild = _client.GetGuild(guildId); var guild = _client.GetGuild(guildId);
var user = guild?.GetUser(userId); var user = guild?.GetUser(userId);
var ch = guild?.GetTextChannel(channelId);
if (guild is null || user is null) if (guild is null || user is null)
return; return;
if (isServer) if (isServer)
{ {
if (notifyLoc == XpNotificationLocation.Dm) var model = new LevelUpNotifyModel()
{ {
await _sender.Response(user) GuildId = guildId,
.Confirm(_strings.GetText(strs.level_up_dm(user.Mention, UserId = userId,
Format.Bold(newLevel.ToString()), ChannelId = channelId,
Format.Bold(guild.ToString() ?? "-")), Level = newLevel
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
}; };
await _notifySub.NotifyAsync(model, true);
if (chan is null) return;
return;
await _sender.Response(chan)
.Confirm(_strings.GetText(strs.level_up_global(user.Mention,
Format.Bold(newLevel.ToString())),
guild.Id))
.SendAsync();
} }
} }
@@ -595,7 +563,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
return await uow return await uow
.UserXpStats .UserXpStats
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp) .OrderByDescending(x => x.Xp)
.Skip(page * 10) .Skip(page * 10)
.Take(10) .Take(10)
.ToArrayAsyncLinqToDB(); .ToArrayAsyncLinqToDB();
@@ -606,7 +574,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>() return await uow.Set<UserXpStats>()
.Where(x => x.GuildId == guildId && x.UserId.In(users)) .Where(x => x.GuildId == guildId && x.UserId.In(users))
.OrderByDescending(x => x.Xp + x.AwardedXp) .OrderByDescending(x => x.Xp)
.Skip(page * 10) .Skip(page * 10)
.Take(10) .Take(10)
.ToArrayAsyncLinqToDB(); .ToArrayAsyncLinqToDB();
@@ -635,35 +603,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
.ToArrayAsyncLinqToDB(); .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) private Task Client_OnGuildAvailable(SocketGuild guild)
{ {
Task.Run(async () => Task.Run(async () =>
@@ -903,7 +842,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
var usr = uow.GetOrCreateUserXpStats(guildId, userId); var usr = uow.GetOrCreateUserXpStats(guildId, userId);
usr.AwardedXp += amount; usr.Xp += amount;
uow.SaveChanges(); uow.SaveChanges();
} }
@@ -949,7 +888,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
return new(du, return new(du,
stats, stats,
new(totalXp), new(totalXp),
new(stats.Xp + stats.AwardedXp), new(stats.Xp),
globalRank, globalRank,
guildRank); guildRank);
} }
@@ -1192,19 +1131,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
outlinePen)); 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); var rankPen = new SolidPen(Color.White, 1);
//ranking //ranking
if (template.User.GlobalRank.Show) if (template.User.GlobalRank.Show)
@@ -1671,14 +1597,27 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
&& (guildUsers == null || guildUsers.Contains(x.UserId))) && (guildUsers == null || guildUsers.Contains(x.UserId)))
.CountAsyncLinqToDB(); .CountAsyncLinqToDB();
} }
}
public enum BuyResult public async Task SetLevelAsync(ulong guildId, ulong userId, int level)
{ {
Success, var lvlStats = LevelStats.CreateForLevel(level);
XpShopDisabled, await using var ctx = _db.GetDbContext();
AlreadyOwned, await ctx.GetTable<UserXpStats>()
InsufficientFunds, .InsertOrUpdateAsync(() => new()
UnknownItem, {
InsufficientPatronTier, 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> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.2.2</Version> <Version>5.3.3</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@@ -29,7 +29,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/> <PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/>
<PackageReference Include="CommandLineParser" Version="2.9.1"/> <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="CoreCLR-NCalc" Version="3.1.246"/>
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/> <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/> <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 public enum AddRemove
{ {
Add = int.MinValue, Add = 0,
Remove = int.MinValue + 1, Remove = 1,
Rem = int.MinValue + 1, Rem = 1,
Rm = int.MinValue + 1 Rm = 1,
} }

View File

@@ -13,7 +13,7 @@ namespace NadekoBot.Common.Configs;
public sealed partial class BotConfig : ICloneable<BotConfig> public sealed partial class BotConfig : ICloneable<BotConfig>
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 8; public int Version { get; set; } = 9;
[Comment(""" [Comment("""
Most commands, when executed, have a small colored line 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""")] [Comment("""List of modules and commands completely blocked on the bot""")]
public BlockedConfig Blocked { get; set; } 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""")] [Comment("""Which string will be used to recognize the commands""")]
public string Prefix { get; set; } public string Prefix { get; set; }

View File

@@ -157,6 +157,9 @@ public sealed class DoAsUserMessage : IUserMessage
public MessageCallData? CallData public MessageCallData? CallData
=> _msg.CallData; => _msg.CallData;
public IReadOnlyCollection<MessageSnapshot> ForwardedMessages
=> _msg.ForwardedMessages;
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null) public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
{ {
return _msg.ModifyAsync(func, options); return _msg.ModifyAsync(func, options);

View File

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

View File

@@ -9,31 +9,31 @@ public sealed class MessageSenderService : IMessageSenderService, INService
public MessageSenderService( public MessageSenderService(
IBotStrings bs, IBotStrings bs,
BotConfigService bcs,
DiscordSocketClient client, DiscordSocketClient client,
IGuildColorsService gcs) IGuildColorsService gcs,
BotConfigService bcs)
{ {
_bs = bs; _bs = bs;
_bcs = bcs;
_client = client; _client = client;
_gcs = gcs; _gcs = gcs;
_bcs = bcs;
} }
public ResponseBuilder Response(IMessageChannel channel) public ResponseBuilder Response(IMessageChannel channel)
=> new ResponseBuilder(_bs, _bcs, _client) => new ResponseBuilder(_bs, this, _client)
.Channel(channel); .Channel(channel);
public ResponseBuilder Response(ICommandContext ctx) public ResponseBuilder Response(ICommandContext ctx)
=> new ResponseBuilder(_bs, _bcs, _client) => new ResponseBuilder(_bs, this, _client)
.Context(ctx); .Context(ctx);
public ResponseBuilder Response(IUser user) public ResponseBuilder Response(IUser user)
=> new ResponseBuilder(_bs, _bcs, _client) => new ResponseBuilder(_bs, this, _client)
.User(user); .User(user);
public ResponseBuilder Response(SocketMessageComponent smc) public ResponseBuilder Response(SocketMessageComponent smc)
=> new ResponseBuilder(_bs, _bcs, _client) => new ResponseBuilder(_bs, this, _client)
.Channel(smc.Channel); .Channel(smc.Channel);
public NadekoEmbedBuilder CreateEmbed(ulong? guildId = null) public NadekoEmbedBuilder CreateEmbed(ulong? guildId = null)
@@ -51,7 +51,7 @@ public class NadekoEmbedBuilder : EmbedBuilder
var bcColors = bcsData.Data.Color; var bcColors = bcsData.Data.Color;
_okColor = guildColors?.Ok ?? bcColors.Ok.ToDiscordColor(); _okColor = guildColors?.Ok ?? bcColors.Ok.ToDiscordColor();
_errorColor = guildColors?.Error ?? bcColors.Error.ToDiscordColor(); _errorColor = guildColors?.Error ?? bcColors.Error.ToDiscordColor();
_pendingColor = guildColors?.Pending ?? bcColors.Pending.ToDiscordColor(); _pendingColor = guildColors?.Warn ?? bcColors.Pending.ToDiscordColor();
} }
public EmbedBuilder WithOkColor() public EmbedBuilder WithOkColor()

View File

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

@@ -108,7 +108,7 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN
{ {
_colors[guildId] = _colors[guildId] with _colors[guildId] = _colors[guildId] with
{ {
Pending = color?.ToDiscordColor() Warn = color?.ToDiscordColor()
}; };
} }
} }
@@ -126,8 +126,8 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN
{ {
var colors = new Colors( var colors = new Colors(
ConvertColor(color.OkColor), ConvertColor(color.OkColor),
ConvertColor(color.ErrorColor), ConvertColor(color.PendingColor),
ConvertColor(color.PendingColor)); ConvertColor(color.ErrorColor));
_colors.TryAdd(color.GuildId, colors); _colors.TryAdd(color.GuildId, colors);
} }

View File

@@ -10,4 +10,4 @@ public interface IGuildColorsService
Task SetPendingColor(ulong guildId, Rgba32? color); 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; c.IgnoreOtherBots = true;
}); });
if(data.Version < 8) if(data.Version < 9)
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 8; c.Version = 9;
}); });
} }
} }

View File

@@ -2,7 +2,7 @@
namespace NadekoBot.Common.TypeReaders.Models; namespace NadekoBot.Common.TypeReaders.Models;
public class StoopidTime public class ParsedTimespan
{ {
private static readonly Regex _regex = new( 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)?$", @"^(?:(?<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 string Input { get; set; } = string.Empty;
public TimeSpan Time { get; set; } = default; 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); 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; => st?.Time;
public static implicit operator StoopidTime(TimeSpan ts) public static implicit operator ParsedTimespan(TimeSpan ts)
=> new() => new()
{ {
Input = ts.ToString(), Input = ts.ToString(),

View File

@@ -3,20 +3,20 @@ using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Common.TypeReaders; 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)) 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 try
{ {
var time = StoopidTime.FromInput(input); var time = ParsedTimespan.FromInput(input);
return new(TypeReaderResult.FromSuccess(time)); return new(TypeReaderResult.FromSuccess(time));
} }
catch (Exception ex) 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, MsgType type,
bool ephemeral = false) bool ephemeral = false)
{ {
var embed = sender.CreateEmbed().WithDescription(text); var embed = sender.CreateEmbed(ch.GuildId).WithDescription(text);
embed = (type switch embed = (type switch
{ {

View File

@@ -715,6 +715,8 @@ color:
avatar: avatar:
- avatar - avatar
- av - av
banner:
- banner
translate: translate:
- translate - translate
- trans - trans
@@ -1039,6 +1041,12 @@ gamevoicechannel:
- gvc - gvc
shoplistadd: shoplistadd:
- shoplistadd - shoplistadd
dmcommand:
- dmcommand
- dmcmd
dmmodule:
- dmmodule
- dmmod
globalcommand: globalcommand:
- globalcommand - globalcommand
- gcmd - gcmd
@@ -1089,9 +1097,6 @@ xpexclusionlist:
xpexclude: xpexclude:
- xpexclude - xpexclude
- xpex - xpex
xpnotify:
- xpnotify
- xpn
xpleveluprewards: xpleveluprewards:
- xplvluprewards - xplvluprewards
- xprews - xprews
@@ -1121,6 +1126,8 @@ xpshopbuy:
- xpshopbuy - xpshopbuy
xpshopuse: xpshopuse:
- xpshopuse - xpshopuse
xplevelset:
- xplevelset
clubcreate: clubcreate:
- clubcreate - clubcreate
clubtransfer: clubtransfer:
@@ -1533,4 +1540,23 @@ servercolorpending:
- pending - pending
- warn - warn
- warning - 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 # DO NOT CHANGE
version: 8 version: 9
# Most commands, when executed, have a small colored line # Most commands, when executed, have a small colored line
# next to the response. The color depends whether the command # next to the response. The color depends whether the command
# is completed, errored or in progress (pending) # is completed, errored or in progress (pending)
@@ -78,6 +78,10 @@ helpText: |-
blocked: blocked:
commands: [] commands: []
modules: [] 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 # Which string will be used to recognize the commands
prefix: . prefix: .
# Whether the bot will rotate through all specified statuses. # Whether the bot will rotate through all specified statuses.

View File

@@ -961,6 +961,56 @@
"MuteMembers Server Permission" "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": [ "Aliases": [
".dpo" ".dpo"
@@ -1523,6 +1573,22 @@
"Administrator Server Permission" "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": [ "Aliases": [
".iam" ".iam"
@@ -1648,9 +1714,9 @@
".sar excl", ".sar excl",
".sar tesar" ".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": [ "Usage": [
".sar exclusive" ".sar exclusive 1"
], ],
"Submodule": "sar", "Submodule": "sar",
"Module": "Administration", "Module": "Administration",
@@ -3274,6 +3340,21 @@
"Options": null, "Options": null,
"Requirements": [] "Requirements": []
}, },
{
"Aliases": [
".winlb",
".wins"
],
"Description": "Shows the biggest wins leaderboard",
"Usage": [
".winlb",
".winlb 5"
],
"Submodule": "BetStatsCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".gamblestats", ".gamblestats",
@@ -3915,6 +3996,20 @@
"Options": null, "Options": null,
"Requirements": [] "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": [ "Aliases": [
".acrophobia", ".acrophobia",
@@ -5522,6 +5617,38 @@
"Bot Owner Only" "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": [ "Aliases": [
".resetperms" ".resetperms"
@@ -5791,6 +5918,19 @@
"Options": null, "Options": null,
"Requirements": [] "Requirements": []
}, },
{
"Aliases": [
".banner"
],
"Description": "Shows a mentioned person's banner.",
"Usage": [
".banner @Someone"
],
"Submodule": "Searches",
"Module": "Searches",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".wikia", ".wikia",
@@ -7778,21 +7918,6 @@
"Options": null, "Options": null,
"Requirements": [] "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": [ "Aliases": [
".xpexclude", ".xpexclude",
@@ -7854,6 +7979,21 @@
"Options": null, "Options": null,
"Requirements": [] "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": [ "Aliases": [
".xpadd" ".xpadd"

View File

@@ -368,7 +368,7 @@ sargroupname:
- group: - group:
desc: "The group number" desc: "The group number"
name: name:
desc: "The name to assign." desc: "The optional name to assign, clears the name if no name is provided."
sargroupdelete: sargroupdelete:
desc: "Deletes a self-assignable role group" desc: "Deletes a self-assignable role group"
ex: ex:
@@ -377,11 +377,14 @@ sargroupdelete:
- group: - group:
desc: "The number of the group to delete." desc: "The number of the group to delete."
sarexclusive: 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: ex:
- '' - '1'
params: params:
- { } - group:
desc: "The number of the group to set exclusive."
sarrolelevelreq: sarrolelevelreq:
desc: Set a level requirement on a self-assignable role. desc: Set a level requirement on a self-assignable role.
ex: ex:
@@ -2170,6 +2173,13 @@ avatar:
params: params:
- usr: - usr:
desc: "The user whose avatar is being displayed." 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: translate:
desc: Translates text from the given language to the destination language. desc: Translates text from the given language to the destination language.
ex: ex:
@@ -3377,14 +3387,28 @@ globalcommand:
- '{0}stats' - '{0}stats'
params: params:
- cmd: - cmd:
desc: "The type of command or expression being toggled." desc: "The command or expression being toggled."
globalmodule: globalmodule:
desc: Toggles whether a module can be used on any server. desc: Toggles whether a module can be used on any server.
ex: ex:
- 'Gambling' - 'Gambling'
params: params:
- module: - 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: globalpermlist:
desc: Lists global permissions set by the bot owner. desc: Lists global permissions set by the bot owner.
ex: ex:
@@ -3515,17 +3539,6 @@ xpexclude:
desc: "The ID of the channel to exclude from XP tracking." desc: "The ID of the channel to exclude from XP tracking."
channel: channel:
desc: "The ID of the channel to exclude from XP tracking." 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: xpleveluprewards:
desc: Shows currently set level up rewards. desc: Shows currently set level up rewards.
ex: ex:
@@ -4821,3 +4834,74 @@ servercolorpending:
params: params:
- color: - color:
desc: "The hex of the color to set" 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.", "gcmd_remove": "Command {0} has been enabled on all servers.",
"gmod_add": "Module {0} has been disabled on all servers.", "gmod_add": "Module {0} has been disabled on all servers.",
"gmod_remove": "Module {0} has been enabled 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.", "lgp_none": "No blocked commands or modules.",
"cant_read_or_send": "You can't read from or send messages to that channel.", "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.", "quotes_notfound": "No quotes found matching the quote ID specified.",
@@ -1083,7 +1087,7 @@
"giveaway_starting": "Starting giveaway...", "giveaway_starting": "Starting giveaway...",
"winner": "Winner", "winner": "Winner",
"giveaway_list": "List of active giveways", "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_list": "Todo List",
"todo_stats": "{0} items | {1} completed | {2} remaining", "todo_stats": "{0} items | {1} completed | {2} remaining",
"todo_add_max_limit": "You'reached the maximum amount of todos you can have.", "todo_add_max_limit": "You'reached the maximum amount of todos you can have.",
@@ -1098,7 +1102,7 @@
"search_results": "Search results", "search_results": "Search results",
"queue_search_results": "Type the number of the search result to queue up that track.", "queue_search_results": "Type the number of the search result to queue up that track.",
"overloads": "Overloads", "overloads": "Overloads",
"honeypot_on": "Honeypot enabled on this channel." , "honeypot_on": "Honeypot enabled on this channel.",
"honeypot_off": "Honeypot disabled.", "honeypot_off": "Honeypot disabled.",
"afk_set": "AFK message set. Type a message in any channel to clear.", "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.", "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_hint": "Use `{0}nczoom x y` command to zoom in. Image is {1}x{2} pixels.",
"nc_insuff_payment": "Invalid payment.", "nc_insuff_payment": "Invalid payment.",
"invalid_img_size": "Image must to be {0}x{1} pixels.", "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_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.", "trfl_disabled": "Flag translation disabled.",
"rakeback_claimed": "You've claimed {0} as rakeback!", "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.", "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_not_found": "Group with that number doesn't exist.",
"sar_group_deleted": "Group {0} deleted.", "sar_group_deleted": "Group {0} deleted.",
"choose_one": "Choose one" , "choose_one": "Choose one",
"requires_role": "Requires role: {0}", "requires_role": "Requires role: {0}",
"invalid_message_id": "Invalid Message Id.", "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_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_not_found": "No button role found on that message.",
"btnrole_none": "There are no button roles on this page.", "btnrole_none": "There are no button roles on this page.",
@@ -1135,5 +1139,25 @@
"no_last_queued_found": "No last queued track found.", "no_last_queued_found": "No last queued track found.",
"wrongsong_success": "Oops! Wrong song removed: {0}", "wrongsong_success": "Oops! Wrong song removed: {0}",
"server_not_found": "Server not found.", "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."
} }