mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e40c9335c1 | ||
|
d921b6889d | ||
|
aaef365bdc | ||
|
a01a646cbf | ||
|
5bee5e63d2 | ||
|
815e318610 | ||
|
634c6c99ee | ||
|
6b37b49439 | ||
|
f42afa7eae | ||
|
ccae1c59e9 | ||
|
b0d8137a7a | ||
|
e78a7d0efa | ||
|
1da19a51f6 | ||
|
91eed9dbd8 | ||
|
7ba345b0fc | ||
|
5d775c9589 | ||
|
2bd8ead10c | ||
|
1148ba3e6e | ||
|
5498bec8cc | ||
|
9eed0c6be5 | ||
|
acf6b7cf58 | ||
|
8598419c5f | ||
|
758093eb32 | ||
|
f44dd03f1a | ||
|
8ac5ec9f57 | ||
|
5209ba802a | ||
|
adfce6670c | ||
|
f8fbc71985 | ||
|
d4e2516a17 | ||
|
f62a67e2e6 | ||
|
7895b5e702 | ||
|
28c8ccfb5f | ||
|
79026e0c27 | ||
|
8b228b840a | ||
|
f61c1a159d | ||
|
2cdcdb2b23 | ||
|
8d0a3ecb20 | ||
|
97f1405a94 | ||
|
0622236523 | ||
|
0f240925e8 | ||
|
d583e2b99a | ||
|
2d72f6f498 | ||
|
5143e42dff | ||
|
790c36df8d | ||
|
15709bc8fb | ||
|
f280f72227 | ||
|
4069368beb | ||
|
e036a2d3c9 | ||
|
62a16f3faf | ||
|
231451f978 | ||
|
2bed4e7eac | ||
|
22eea3fa7b | ||
|
8b1767078e |
157
CHANGELOG.md
157
CHANGELOG.md
@@ -2,6 +2,154 @@
|
|||||||
|
|
||||||
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.6] - 20.01.2025
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added player skill stat when fishing
|
||||||
|
- Starts at 0, goes up to 100
|
||||||
|
- Every time you fish you have a chance to get an extra skill point
|
||||||
|
- Higher skill gives you more chance to catch fish (and therefore less chance to catch trash)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Patrons no longer have `.timely` and `.fish` captcha on the public bot
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Fixed fishing spots again (Your channels will once again change a spot, last time hopefully)
|
||||||
|
- There was a mistake in spot calculation for each channel
|
||||||
|
|
||||||
|
## [5.3.5] - 17.01.2025
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- .sar rm will now accept role ids in case the role was deleted
|
||||||
|
- `.deletewaifus` should work again
|
||||||
|
|
||||||
|
## [5.3.4] - 14.01.2025
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added `.fish` commands
|
||||||
|
- `.fish` - Attempt to catch a fish - different fish live in different places, at different times and during different times of the day
|
||||||
|
- `.fishlist` - Look at your fish catalogue - shows how many of each fish you caught and what was the highest quality - for each caught fish, it also shows its required spot, time of day and weather
|
||||||
|
- `.fishspot` - Shows information about the current fish spot, time of day and weather
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `.timely` fixed captcha sometimes generating only 2 characters
|
||||||
|
|
||||||
|
## [5.3.3] - 15.12.2024
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `.notify` commands are no longer owner only, they now require Admin permissions
|
||||||
|
- `.notify` messages can now mention anyone
|
||||||
|
|
||||||
|
## [5.3.2] - 14.12.2024
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `.banner` should be working properly now with both server and global user banners
|
||||||
|
|
||||||
|
## [5.3.1] - 13.12.2024
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- `.translate` will now use 2 embeds, to allow for longer messages
|
||||||
|
- Added role icon to `.inrole`, if it exists
|
||||||
|
- `.honeypot` will now add a 'Honeypot' as a ban reason.
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `.winlb` looks better, has a title, shows 9 entries now
|
||||||
|
- `.sar ex` help updated
|
||||||
|
- `.banner` partially fixed, it still can't show global banners, but it will show guild ones correctly, in a good enough size
|
||||||
|
- `.sclr` will now show correct color hexes without alpha
|
||||||
|
- `.dmcmd` will now correctly block commands in dms, not globally
|
||||||
|
|
||||||
|
## [5.3.0] - 10.12.2024
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added `.minesweeper` / `.mw` command - spoiler-based minesweeper minigame. Just for fun
|
||||||
|
- Added `.temprole` command - add a role to a user for a certain amount of time, after which the role will be removed
|
||||||
|
- Added `.xplevelset` - you can now set a level for a user in your server
|
||||||
|
- Added `.winlb` command - leaderboard of top gambling wins
|
||||||
|
- Added `.notify` command
|
||||||
|
- Specify an event to be notified about, and the bot will post the specified message in the current channel when the
|
||||||
|
event occurs
|
||||||
|
- A few events supported right now:
|
||||||
|
- `UserLevelUp` when user levels up in the server
|
||||||
|
- `AddRoleReward` when a role is added to a user through .xpreward system
|
||||||
|
- `RemoveRoleReward` when a role is removed from a user through .xpreward system
|
||||||
|
- `Protection` when antialt, antiraid or antispam protection is triggered
|
||||||
|
- Added `.banner` command to see someone's banner
|
||||||
|
- Selfhosters:
|
||||||
|
- Added `.dmmod` and `.dmcmd` - you can now disable or enable whether commands or modules can be executed in bot's
|
||||||
|
DMs
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Giveaway improvements
|
||||||
|
- Now mentions winners in a separate message
|
||||||
|
- Shows the timestamp of when the giveaway ends
|
||||||
|
- Xp Changes
|
||||||
|
- Removed awarded xp (the number in the brackets on the xp card)
|
||||||
|
- Awarded xp, (or the new level set) now directly apply to user's real xp
|
||||||
|
- Server xp notifications are now set by the server admin/manager in a specified channel
|
||||||
|
- `.sclr show` will now show hex code of the current color
|
||||||
|
- Queueing a song will now restart the playback if the queue is on the last track and stopped (there were no more tracks
|
||||||
|
to play)
|
||||||
|
- `.translate` will now use 2 embeds instead of 1
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- .setstream and .setactivity will now pause .ropl (rotating statuses)
|
||||||
|
- Fixed `.sar ex` help description
|
||||||
|
|
||||||
|
## Removed
|
||||||
|
|
||||||
|
- `.xpnotify` command, superseded by `.notify`, although as of right now you can't post user's level up in the same
|
||||||
|
channel user last typed, because you have to specify a channel where the notify messages will be posted
|
||||||
|
|
||||||
|
## [5.2.4] - 27.11.2024
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- More fixes for .sclr
|
||||||
|
- `.iamn` fixed
|
||||||
|
|
||||||
|
## [5.2.3] - 27.11.2024
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `.iam` Fixed
|
||||||
|
- `.sclr` will now properly change color on many commands it didn't work previously
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `.rps` now also has bet amount in the result, like other gambling commands
|
||||||
|
|
||||||
|
## [5.2.2] - 27.11.2024
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Button roles are now non-exclusive by default
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed sar migration, again (this time correctly)
|
||||||
|
- Fixed `.sclr` not updating unless bot is restarted, the changes should be immediate now for warn and error
|
||||||
|
- Fixed group buttons exclusivity message always saying groups are exclusive
|
||||||
|
|
||||||
|
## [5.2.1] - 26.11.2024
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed old self assigned missing
|
||||||
|
|
||||||
## [5.2.0] - 26.11.2024
|
## [5.2.0] - 26.11.2024
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -18,7 +166,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
|||||||
- 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
|
||||||
|
left side of the message the bot sends)
|
||||||
- Use `.h .sclr` to see the list of commands
|
- Use `.h .sclr` to see the list of commands
|
||||||
- `.sclr show` will show the current server colors
|
- `.sclr show` will show the current server colors
|
||||||
- `.sclr ok <color hex>` to set ok color
|
- `.sclr ok <color hex>` to set ok color
|
||||||
@@ -32,7 +181,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
|||||||
- 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
|
||||||
|
per group.
|
||||||
- `.sar groupname`
|
- `.sar groupname`
|
||||||
- Sets a self assignable role group name. Provide no name to remove.
|
- Sets a self assignable role group name. Provide no name to remove.
|
||||||
- `.sar remove`
|
- `.sar remove`
|
||||||
@@ -40,7 +190,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
|||||||
- `.sar list`
|
- `.sar list`
|
||||||
- Lists self-assignable roles. Shows 20 roles per page.
|
- Lists self-assignable roles. Shows 20 roles per page.
|
||||||
- `.sar exclusive`
|
- `.sar exclusive`
|
||||||
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.
|
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role
|
||||||
|
per group.
|
||||||
- `.sar rolelvlreq`
|
- `.sar rolelvlreq`
|
||||||
- Set a level requirement on a self-assignable role.
|
- Set a level requirement on a self-assignable role.
|
||||||
- `.sar grouprolereq`
|
- `.sar grouprolereq`
|
||||||
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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)*.
|
||||||
|
@@ -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]
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
## 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) |
|
||||||
@@ -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 @@
|
|||||||

|

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

|

|
||||||
- **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.
|
||||||
|
77
docs/guides/windows-source-guide.md
Normal file
77
docs/guides/windows-source-guide.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
## Setting Up NadekoBot on Windows from source
|
||||||
|
|
||||||
|
1. Prerequisites
|
||||||
|
|
||||||
|
- Windows 10 or later (64-bit)
|
||||||
|
- [.net 8 sdk](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||||
|
- If you want nadeko to play music: [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required, you may install them later)
|
||||||
|
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
|
||||||
|
- **Optional** Any code editor, for example [Visual Studio Code](https://code.visualstudio.com/Download)
|
||||||
|
- You'll need to at least modify creds.yml, notepad is inadequate
|
||||||
|
|
||||||
|
|
||||||
|
##### Installation Instructions
|
||||||
|
|
||||||
|
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and
|
||||||
|
|
||||||
|
|
||||||
|
0. Navigate to the location where you want to install the bot
|
||||||
|
- for example, type `cd ~/Desktop/` and press enter
|
||||||
|
1. `git clone https://gitlab.com/kwoth/nadekobot -b v5 --depth 1`
|
||||||
|
2. `cd nadekobot`
|
||||||
|
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
|
||||||
|
4. `cd output`
|
||||||
|
5. `cp creds_example.yml creds.yml`
|
||||||
|
6. "You're done installing, you may now proceed to set up your bot's credentials by following the [#creds-guide]
|
||||||
|
- Once done, come back here and run the last command
|
||||||
|
8. Run the bot `dotnet NadekoBot.dll`
|
||||||
|
9. 🎉 Enjoy
|
||||||
|
|
||||||
|
##### Update Instructions
|
||||||
|
|
||||||
|
Open PowerShell as described above and run the following commands:
|
||||||
|
|
||||||
|
1. Stop the bot
|
||||||
|
- ⚠️ Make sure you don't have your database, credentials or any other nadekobot folder open in some application, this might prevent some of the steps from executing succesfully
|
||||||
|
2. Navigate to your bot's folder, example:
|
||||||
|
- `cd ~/Desktop/nadekobot`
|
||||||
|
3. Pull the new version, and make sure you're on the v5 branch
|
||||||
|
- *⚠️ If you're on v4, you must run these commands, if not, you may skip them.*
|
||||||
|
- `git remote set-branches origin '*'`
|
||||||
|
- `git fetch -v --depth=1`
|
||||||
|
- `git checkout v5`
|
||||||
|
- `git pull`
|
||||||
|
- ⚠️ If this fails, you may want to stash or remove your code changes if you don't know how to resolve merge conflicts
|
||||||
|
4. **Backup** old output in case your data is overwritten
|
||||||
|
- `cp -r -fo output/ output-old`
|
||||||
|
5. Build the bot again
|
||||||
|
- `dotnet publish -c Release -o output/ src/NadekoBot/`
|
||||||
|
6. Remove old strings and aliases to avoid overwriting the updated versions of those files
|
||||||
|
- ⚠ If you've modified said files, back them up instead
|
||||||
|
- `rm output-old/data/aliases.yml`
|
||||||
|
- `rm -r output-old/data/strings`
|
||||||
|
7. Copy old data, and new strings
|
||||||
|
- `cp -Recurse -Force .\output-old\data\ .\output\`
|
||||||
|
- `cp -Recurse -Force src/NadekoBot/data/strings/ output/data/`
|
||||||
|
8. Copy creds.yml
|
||||||
|
- `cp output-old/creds.yml output/`
|
||||||
|
9. Run the bot
|
||||||
|
- `cd output`
|
||||||
|
- `dotnet NadekoBot.dll`
|
||||||
|
|
||||||
|
🎉 Enjoy
|
||||||
|
|
||||||
|
#### Music prerequisites
|
||||||
|
In order to use music commands, you need ffmpeg and yt-dlp installed.
|
||||||
|
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `ffmpeg.exe` file to `NadekoBot/output`.
|
||||||
|
- [youtube-dlp] - Click to download the `yt-dlp.exe` file, then move `yt-dlp.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `yt-dlp.exe` file to `NadekoBot/system`.
|
||||||
|
|
||||||
|
[Updater]: https://dl.nadeko.bot/v3/
|
||||||
|
[Notepad++]: https://notepad-plus-plus.org/
|
||||||
|
[.net]: https://dotnet.microsoft.com/download/dotnet/8.0
|
||||||
|
[Redis]: https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.msi
|
||||||
|
[Visual C++ 2010 (x86)]: https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe
|
||||||
|
[Visual C++ 2017 (x64)]: https://aka.ms/vs/15/release/vc_redist.x64.exe
|
||||||
|
[ffmpeg-32bit]: https://cdn.nadeko.bot/dl/ffmpeg-32.zip
|
||||||
|
[ffmpeg-64bit]: https://cdn.nadeko.bot/dl/ffmpeg-64.zip
|
||||||
|
[youtube-dlp]: https://github.com/yt-dlp/yt-dlp/releases
|
@@ -16,6 +16,7 @@ To self-host your own Nadeko, use the guides below:
|
|||||||
- [:material-microsoft-windows: Windows guide][windows-guide]
|
- [:material-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
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
88
src/NadekoBot.Tests/FishTests.cs
Normal file
88
src/NadekoBot.Tests/FishTests.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// using System;
|
||||||
|
// using System.Collections.Generic;
|
||||||
|
// using System.Diagnostics;
|
||||||
|
// using System.IO;
|
||||||
|
// using System.Linq;
|
||||||
|
// using Nadeko.Common;
|
||||||
|
// using NadekoBot.Modules.Games;
|
||||||
|
// using NUnit.Framework;
|
||||||
|
//
|
||||||
|
// namespace NadekoBot.Tests;
|
||||||
|
//
|
||||||
|
// public class FishTests
|
||||||
|
// {
|
||||||
|
// [Test]
|
||||||
|
// public void TestWeather()
|
||||||
|
// {
|
||||||
|
// var fs = new FishService(null, null);
|
||||||
|
//
|
||||||
|
// var rng = new Random();
|
||||||
|
//
|
||||||
|
// // output = @"ro+dD:bN0uVqV3ZOAv6r""EFeA'A]u]uSyz2Qd'r#0Vf:5zOX\VgSsF8LgRCL/uOW";
|
||||||
|
// while (true)
|
||||||
|
// {
|
||||||
|
// var output = "";
|
||||||
|
// for (var i = 0; i < 64; i++)
|
||||||
|
// {
|
||||||
|
// var c = (char)rng.Next(33, 123);
|
||||||
|
// output += c;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// output = "";
|
||||||
|
// var weathers = new List<FishingWeather>();
|
||||||
|
// for (var i = 0; i < 1_000_000; i++)
|
||||||
|
// {
|
||||||
|
// var w = fs.GetWeather(DateTime.UtcNow.AddHours(6 * i), output);
|
||||||
|
// weathers.Add(w);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var vals = weathers.GroupBy(x => x)
|
||||||
|
// .ToDictionary(x => x.Key, x => x.Count());
|
||||||
|
//
|
||||||
|
// var str = weathers.Select(x => (int)x).Join("");
|
||||||
|
// var maxLength = MaxLength(str);
|
||||||
|
//
|
||||||
|
// if (maxLength < 12)
|
||||||
|
// {
|
||||||
|
// foreach (var v in vals)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"{v.Key}: {v.Value}");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Console.WriteLine(output);
|
||||||
|
// Console.WriteLine(maxLength);
|
||||||
|
//
|
||||||
|
// File.WriteAllText("data.txt", weathers.Select(x => (int)x).Join(""));
|
||||||
|
//
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // string with same characters
|
||||||
|
// static int MaxLength(String s)
|
||||||
|
// {
|
||||||
|
// int ans = 1, temp = 1;
|
||||||
|
//
|
||||||
|
// // Traverse the string
|
||||||
|
// for (int i = 1; i < s.Length; i++)
|
||||||
|
// {
|
||||||
|
// // If character is same as
|
||||||
|
// // previous increment temp value
|
||||||
|
// if (s[i] == s[i - 1])
|
||||||
|
// {
|
||||||
|
// ++temp;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// ans = Math.Max(ans, temp);
|
||||||
|
// temp = 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ans = Math.Max(ans, temp);
|
||||||
|
//
|
||||||
|
// // Return the required answer
|
||||||
|
// return ans;
|
||||||
|
// }
|
||||||
|
// }
|
@@ -1,4 +1,5 @@
|
|||||||
using Nadeko.Common;
|
using System;
|
||||||
|
using Nadeko.Common;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace NadekoBot.Tests
|
namespace NadekoBot.Tests
|
||||||
@@ -120,5 +121,12 @@ namespace NadekoBot.Tests
|
|||||||
num = new kwum(int.MaxValue);
|
num = new kwum(int.MaxValue);
|
||||||
Assert.AreEqual("3zzzzzz", num.ToString());
|
Assert.AreEqual("3zzzzzz", num.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPower()
|
||||||
|
{
|
||||||
|
var num = new kwum((int)Math.Pow(32, 2));
|
||||||
|
Assert.AreEqual("322", num.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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
|
@@ -218,12 +218,12 @@ public sealed class Bot : IBot
|
|||||||
catch (HttpException ex)
|
catch (HttpException ex)
|
||||||
{
|
{
|
||||||
LoginErrorHandler.Handle(ex);
|
LoginErrorHandler.Handle(ex);
|
||||||
Helpers.ReadErrorAndExit(3);
|
Helpers.ReadErrorAndExit(101);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoginErrorHandler.Handle(ex);
|
LoginErrorHandler.Handle(ex);
|
||||||
Helpers.ReadErrorAndExit(4);
|
Helpers.ReadErrorAndExit(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
await clientReady.Task.ConfigureAwait(false);
|
await clientReady.Task.ConfigureAwait(false);
|
||||||
@@ -275,7 +275,7 @@ public sealed class Bot : IBot
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Error adding services");
|
Log.Error(ex, "Error adding services");
|
||||||
Helpers.ReadErrorAndExit(9);
|
Helpers.ReadErrorAndExit(103);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s",
|
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s",
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
@@ -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;
|
public static LevelStats CreateForLevel(long level)
|
||||||
LevelXp = xp - totalXp;
|
=> new(GetTotalXpReqForLevel(level));
|
||||||
RequiredXp = required;
|
|
||||||
}
|
public static long GetTotalXpReqForLevel(long level)
|
||||||
|
=> ((9 * level * level) + (63 * level)) / 2;
|
||||||
|
|
||||||
|
public static long GetLevelByTotalXp(long totalXp)
|
||||||
|
=> (long)((-7.0 / 2) + (1 / 6.0 * Math.Sqrt((8 * totalXp) + 441)));
|
||||||
}
|
}
|
@@ -36,29 +36,37 @@ public class GuildConfig : DbEntity
|
|||||||
public HashSet<FilterChannelId> FilterInvitesChannelIds { get; set; } = new();
|
public HashSet<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; }
|
||||||
|
24
src/NadekoBot/Db/Models/Notify.cs
Normal file
24
src/NadekoBot/Db/Models/Notify.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
public class Notify
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
public ulong ChannelId { get; set; }
|
||||||
|
public NotifyType Type { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(10_000)]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NotifyType
|
||||||
|
{
|
||||||
|
LevelUp = 0,
|
||||||
|
Protection = 1, Prot = 1,
|
||||||
|
AddRoleReward = 2,
|
||||||
|
RemoveRoleReward = 3,
|
||||||
|
}
|
12
src/NadekoBot/Db/Models/roles/TempRole.cs
Normal file
12
src/NadekoBot/Db/Models/roles/TempRole.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
public class TempRole
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
public bool Remove { get; set; }
|
||||||
|
public ulong RoleId { get; set; }
|
||||||
|
public ulong UserId { get; set; }
|
||||||
|
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
}
|
@@ -6,6 +6,4 @@ public class UserXpStats : DbEntity
|
|||||||
public ulong UserId { get; set; }
|
public ulong 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; }
|
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
namespace NadekoBot.Db.Models;
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
public enum XpNotificationLocation
|
public enum XpNotificationLocation
|
||||||
{
|
{
|
||||||
|
@@ -74,6 +74,38 @@ public abstract class NadekoContext : DbContext
|
|||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
// load all entities from current assembly
|
||||||
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(NadekoContext).Assembly);
|
||||||
|
|
||||||
|
#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,14 +167,19 @@ 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.UserId,
|
||||||
x.Game
|
x.Game
|
||||||
})
|
})
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
ubs.HasIndex(x => x.MaxWin)
|
||||||
|
.IsUnique(false);
|
||||||
|
});
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Flag Translate
|
#region Flag Translate
|
||||||
@@ -449,7 +486,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
|
||||||
|
|
||||||
|
@@ -5,8 +5,21 @@ 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)
|
||||||
{
|
{
|
||||||
|
if (migrationBuilder.IsNpgsql())
|
||||||
|
return;
|
||||||
|
|
||||||
migrationBuilder.Sql("""
|
migrationBuilder.Sql("""
|
||||||
INSERT INTO GroupName (Number, GuildConfigId)
|
INSERT INTO GroupName (Number, GuildConfigId)
|
||||||
SELECT DISTINCT "Group", GC.Id
|
SELECT DISTINCT "Group", GC.Id
|
||||||
@@ -21,14 +34,13 @@ public static class MigrationQueries
|
|||||||
INNER JOIN GuildConfigs as GC ON GN.GuildConfigId = GC.Id;
|
INNER JOIN GuildConfigs as GC ON GN.GuildConfigId = GC.Id;
|
||||||
|
|
||||||
INSERT INTO Sar (GuildId, RoleId, SarGroupId, LevelReq)
|
INSERT INTO Sar (GuildId, RoleId, SarGroupId, LevelReq)
|
||||||
SELECT SAR.GuildId, SAR.RoleId, MIN(SG2.Id), MIN(SAR.LevelRequirement)
|
SELECT SAR.GuildId, SAR.RoleId, (SELECT Id FROM SarGroup WHERE SG.Number = SarGroup.GroupNumber AND SG.GuildId = SarGroup.GuildId), MIN(SAR.LevelRequirement)
|
||||||
FROM SelfAssignableRoles as SAR
|
FROM SelfAssignableRoles as SAR
|
||||||
INNER JOIN (SELECT GuildId FROM GroupName as gn
|
INNER JOIN (SELECT GuildId, gn.Number FROM GroupName as gn
|
||||||
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
|
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
|
||||||
) as SG
|
) as SG
|
||||||
ON SG.GuildId = SAR.GuildId
|
ON SG.GuildId = SAR.GuildId
|
||||||
INNER JOIN GroupName as SG2
|
WHERE SG.Number IN (SELECT GroupNumber FROM SarGroup WHERE Sar.GuildId = SarGroup.GuildId)
|
||||||
ON SG2.Number = SAR."Group"
|
|
||||||
GROUP BY SAR.GuildId, SAR.RoleId;
|
GROUP BY SAR.GuildId, SAR.RoleId;
|
||||||
|
|
||||||
INSERT INTO SarAutoDelete (GuildId, IsEnabled)
|
INSERT INTO SarAutoDelete (GuildId, IsEnabled)
|
||||||
|
4117
src/NadekoBot/Migrations/PostgreSql/20241207150050_awardedxp-temprole-notify.Designer.cs
generated
Normal file
4117
src/NadekoBot/Migrations/PostgreSql/20241207150050_awardedxp-temprole-notify.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class awardedxptemprolenotify : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_userxpstats_awardedxp",
|
||||||
|
table: "userxpstats");
|
||||||
|
|
||||||
|
MigrationQueries.MergeAwardedXp(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "awardedxp",
|
||||||
|
table: "userxpstats");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "notifyonlevelup",
|
||||||
|
table: "userxpstats");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "notify",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_notify", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_notify_guildid_type", x => new { x.guildid, x.type });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "temprole",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
remove = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
expiresat = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_temprole", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_temprole_guildid_userid_roleid", x => new { x.guildid, x.userid, x.roleid });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_userbetstats_maxwin",
|
||||||
|
table: "userbetstats",
|
||||||
|
column: "maxwin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_temprole_expiresat",
|
||||||
|
table: "temprole",
|
||||||
|
column: "expiresat");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "notify");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "temprole");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_userbetstats_maxwin",
|
||||||
|
table: "userbetstats");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "awardedxp",
|
||||||
|
table: "userxpstats",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "notifyonlevelup",
|
||||||
|
table: "userxpstats",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_userxpstats_awardedxp",
|
||||||
|
table: "userxpstats",
|
||||||
|
column: "awardedxp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4151
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.Designer.cs
generated
Normal file
4151
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.cs
Normal file
39
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "fishcatch",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
fishid = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
count = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
maxstars = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_fishcatch", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_fishcatch_userid_fishid", x => new { x.userid, x.fishid });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "fishcatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4178
src/NadekoBot/Migrations/PostgreSql/20250118235233_fish-skill.Designer.cs
generated
Normal file
4178
src/NadekoBot/Migrations/PostgreSql/20250118235233_fish-skill.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishskill : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "userfishstats",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
skill = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_userfishstats", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_userfishstats_userid",
|
||||||
|
table: "userfishstats",
|
||||||
|
column: "userid",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "userfishstats");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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");
|
||||||
|
|
||||||
@@ -3308,6 +3374,67 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.ToTable("xpshopowneditem", (string)null);
|
b.ToTable("xpshopowneditem", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Modules.Games.FishCatch", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Count")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("count");
|
||||||
|
|
||||||
|
b.Property<int>("FishId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("fishid");
|
||||||
|
|
||||||
|
b.Property<int>("MaxStars")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("maxstars");
|
||||||
|
|
||||||
|
b.Property<decimal>("UserId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("userid");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_fishcatch");
|
||||||
|
|
||||||
|
b.HasAlternateKey("UserId", "FishId")
|
||||||
|
.HasName("ak_fishcatch_userid_fishid");
|
||||||
|
|
||||||
|
b.ToTable("fishcatch", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Modules.Games.UserFishStats", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Skill")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("skill");
|
||||||
|
|
||||||
|
b.Property<decimal>("UserId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("userid");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_userfishstats");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_userfishstats_userid");
|
||||||
|
|
||||||
|
b.ToTable("userfishstats", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -3416,6 +3543,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");
|
||||||
|
3173
src/NadekoBot/Migrations/Sqlite/20241207150041_awardedxp-temprole-notify.Designer.cs
generated
Normal file
3173
src/NadekoBot/Migrations/Sqlite/20241207150041_awardedxp-temprole-notify.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class awardedxptemprolenotify : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_UserXpStats_AwardedXp",
|
||||||
|
table: "UserXpStats");
|
||||||
|
|
||||||
|
MigrationQueries.MergeAwardedXp(migrationBuilder);
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AwardedXp",
|
||||||
|
table: "UserXpStats");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "NotifyOnLevelUp",
|
||||||
|
table: "UserXpStats");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Notify",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Notify", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_Notify_GuildId_Type", x => new { x.GuildId, x.Type });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TempRole",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
Remove = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TempRole", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_TempRole_GuildId_UserId_RoleId", x => new { x.GuildId, x.UserId, x.RoleId });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserBetStats_MaxWin",
|
||||||
|
table: "UserBetStats",
|
||||||
|
column: "MaxWin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TempRole_ExpiresAt",
|
||||||
|
table: "TempRole",
|
||||||
|
column: "ExpiresAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Notify");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TempRole");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_UserBetStats_MaxWin",
|
||||||
|
table: "UserBetStats");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "AwardedXp",
|
||||||
|
table: "UserXpStats",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "NotifyOnLevelUp",
|
||||||
|
table: "UserXpStats",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserXpStats_AwardedXp",
|
||||||
|
table: "UserXpStats",
|
||||||
|
column: "AwardedXp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3198
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.Designer.cs
generated
Normal file
3198
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.cs
Normal file
38
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FishCatch",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
FishId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Count = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
MaxStars = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FishCatch", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_FishCatch_UserId_FishId", x => new { x.UserId, x.FishId });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FishCatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3218
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.Designer.cs
generated
Normal file
3218
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.cs
Normal file
41
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishskill : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserFishStats",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
Skill = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserFishStats", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserFishStats_UserId",
|
||||||
|
table: "UserFishStats",
|
||||||
|
column: "UserId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserFishStats");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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");
|
||||||
@@ -2459,6 +2508,51 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("XpShopOwnedItem");
|
b.ToTable("XpShopOwnedItem");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Modules.Games.FishCatch", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Count")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("FishId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MaxStars")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasAlternateKey("UserId", "FishId");
|
||||||
|
|
||||||
|
b.ToTable("FishCatch");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Modules.Games.UserFishStats", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Skill")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("UserFishStats");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -2541,6 +2635,8 @@ namespace NadekoBot.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaxWin");
|
||||||
|
|
||||||
b.HasIndex("UserId", "Game")
|
b.HasIndex("UserId", "Game")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
{
|
{
|
||||||
|
23
src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs
Normal file
23
src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public interface INotifyModel
|
||||||
|
{
|
||||||
|
static abstract string KeyName { get; }
|
||||||
|
static abstract NotifyType NotifyType { get; }
|
||||||
|
IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements();
|
||||||
|
|
||||||
|
public virtual bool TryGetGuildId(out ulong guildId)
|
||||||
|
{
|
||||||
|
guildId = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool TryGetUserId(out ulong userId)
|
||||||
|
{
|
||||||
|
userId = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public interface INotifySubscriber
|
||||||
|
{
|
||||||
|
Task NotifyAsync<T>(T data, bool isShardLocal = false)
|
||||||
|
where T : struct, INotifyModel;
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Xp.Services;
|
||||||
|
|
||||||
|
public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel
|
||||||
|
{
|
||||||
|
public static string KeyName
|
||||||
|
=> "notify.reward.addrole";
|
||||||
|
|
||||||
|
public static NotifyType NotifyType
|
||||||
|
=> NotifyType.AddRoleReward;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||||
|
{
|
||||||
|
var model = this;
|
||||||
|
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||||
|
{
|
||||||
|
{ "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() },
|
||||||
|
{ "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() },
|
||||||
|
{ "%event.level%", g => model.Level.ToString() }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetUserId(out ulong userId)
|
||||||
|
{
|
||||||
|
userId = UserId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetGuildId(out ulong guildId)
|
||||||
|
{
|
||||||
|
guildId = GuildId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public record struct LevelUpNotifyModel(
|
||||||
|
ulong GuildId,
|
||||||
|
ulong ChannelId,
|
||||||
|
ulong UserId,
|
||||||
|
long Level) : INotifyModel
|
||||||
|
{
|
||||||
|
public static string KeyName
|
||||||
|
=> "notify.levelup";
|
||||||
|
|
||||||
|
public static NotifyType NotifyType
|
||||||
|
=> NotifyType.LevelUp;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||||
|
{
|
||||||
|
var data = this;
|
||||||
|
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||||
|
{
|
||||||
|
{ "%event.level%", g => data.Level.ToString() },
|
||||||
|
{ "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetGuildId(out ulong guildId)
|
||||||
|
{
|
||||||
|
guildId = GuildId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetUserId(out ulong userId)
|
||||||
|
{
|
||||||
|
userId = UserId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel
|
||||||
|
{
|
||||||
|
public static string KeyName
|
||||||
|
=> "notify.protection";
|
||||||
|
|
||||||
|
public static NotifyType NotifyType
|
||||||
|
=> NotifyType.Protection;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||||
|
{
|
||||||
|
var data = this;
|
||||||
|
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||||
|
{
|
||||||
|
{ "%event.type%", g => data.ProtType.ToString() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetUserId(out ulong userId)
|
||||||
|
{
|
||||||
|
userId = UserId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetGuildId(out ulong guildId)
|
||||||
|
{
|
||||||
|
guildId = GuildId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Xp.Services;
|
||||||
|
|
||||||
|
public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel
|
||||||
|
{
|
||||||
|
public static string KeyName
|
||||||
|
=> "notify.reward.removerole";
|
||||||
|
|
||||||
|
public static NotifyType NotifyType
|
||||||
|
=> NotifyType.RemoveRoleReward;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
|
||||||
|
{
|
||||||
|
var model = this;
|
||||||
|
return new Dictionary<string, Func<SocketGuild, string>>()
|
||||||
|
{
|
||||||
|
{ "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() },
|
||||||
|
{ "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() },
|
||||||
|
{ "%event.level%", g => model.Level.ToString() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetUserId(out ulong userId)
|
||||||
|
{
|
||||||
|
userId = UserId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetGuildId(out ulong guildId)
|
||||||
|
{
|
||||||
|
guildId = GuildId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
114
src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs
Normal file
114
src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
public class NotifyCommands : NadekoModule<NotifyService>
|
||||||
|
{
|
||||||
|
[Cmd]
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public static class NotifyKeys
|
||||||
|
{
|
||||||
|
public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public static class NotifyModelExtensions
|
||||||
|
{
|
||||||
|
public static TypedKey<T> GetTypedKey<T>(this T model)
|
||||||
|
where T : struct, INotifyModel
|
||||||
|
=> new(T.KeyName);
|
||||||
|
}
|
227
src/NadekoBot/Modules/Administration/Notify/NotifyService.cs
Normal file
227
src/NadekoBot/Modules/Administration/Notify/NotifyService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@@ -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]
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -283,7 +283,13 @@ public partial class Administration
|
|||||||
{
|
{
|
||||||
var res = await _service.SetExclusiveButtonRoles(ctx.Guild.Id, messageId, exclusive.Value);
|
var res = await _service.SetExclusiveButtonRoles(ctx.Guild.Id, messageId, exclusive.Value);
|
||||||
|
|
||||||
if (res)
|
if (!res)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.btnrole_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclusive.Value)
|
||||||
{
|
{
|
||||||
await Response().Confirm(strs.btnrole_exclusive).SendAsync();
|
await Response().Confirm(strs.btnrole_exclusive).SendAsync();
|
||||||
}
|
}
|
||||||
|
@@ -126,7 +126,11 @@ public sealed class ButtonRolesService : INService, IReadyExecutor
|
|||||||
Emote = emoteStr,
|
Emote = emoteStr,
|
||||||
Label = string.Empty,
|
Label = string.Empty,
|
||||||
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}",
|
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}",
|
||||||
Exclusive = (uow.GetTable<ButtonRole>().Where(x => x.GuildId == guildId && x.MessageId == messageId).All(x => x.Exclusive))
|
Exclusive = uow.GetTable<ButtonRole>()
|
||||||
|
.Any(x => x.GuildId == guildId && x.MessageId == messageId)
|
||||||
|
&& uow.GetTable<ButtonRole>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
|
||||||
|
.All(x => x.Exclusive)
|
||||||
},
|
},
|
||||||
_ => new()
|
_ => new()
|
||||||
{
|
{
|
||||||
@@ -178,7 +182,6 @@ public sealed class ButtonRolesService : INService, IReadyExecutor
|
|||||||
.UpdateAsync((_) => new()
|
.UpdateAsync((_) => new()
|
||||||
{
|
{
|
||||||
Exclusive = exclusive
|
Exclusive = exclusive
|
||||||
})
|
}) > 0;
|
||||||
> 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
140
src/NadekoBot/Modules/Administration/Role/TempRoleService.cs
Normal file
140
src/NadekoBot/Modules/Administration/Role/TempRoleService.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public class TempRoleService : IReadyExecutor, INService
|
||||||
|
{
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IBotCreds _creds;
|
||||||
|
|
||||||
|
private TaskCompletionSource<bool> _tcs = new();
|
||||||
|
|
||||||
|
public TempRoleService(
|
||||||
|
DbService db,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IBotCreds creds)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_client = client;
|
||||||
|
_creds = creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddTempRoleAsync(
|
||||||
|
ulong guildId,
|
||||||
|
ulong roleId,
|
||||||
|
ulong userId,
|
||||||
|
TimeSpan duration)
|
||||||
|
{
|
||||||
|
if (duration == TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
await uow.GetTable<TempRole>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||||
|
.DeleteAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var until = DateTime.UtcNow.Add(duration);
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
await ctx.GetTable<TempRole>()
|
||||||
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
RoleId = roleId,
|
||||||
|
UserId = userId,
|
||||||
|
Remove = false,
|
||||||
|
ExpiresAt = until
|
||||||
|
},
|
||||||
|
(old) => new()
|
||||||
|
{
|
||||||
|
ExpiresAt = until,
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
UserId = userId,
|
||||||
|
RoleId = roleId
|
||||||
|
});
|
||||||
|
|
||||||
|
_tcs.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var latest = await _db.GetDbContext()
|
||||||
|
.GetTable<TempRole>()
|
||||||
|
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
|
||||||
|
_creds.TotalShards,
|
||||||
|
_client.ShardId))
|
||||||
|
.OrderBy(x => x.ExpiresAt)
|
||||||
|
.FirstOrDefaultAsyncLinqToDB();
|
||||||
|
|
||||||
|
if (latest == default)
|
||||||
|
{
|
||||||
|
await _tcs.Task;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (latest.ExpiresAt > now)
|
||||||
|
{
|
||||||
|
await Task.WhenAny(Task.Delay(latest.ExpiresAt - now), _tcs.Task);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await _db.GetDbContext()
|
||||||
|
.GetTable<TempRole>()
|
||||||
|
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
|
||||||
|
_creds.TotalShards,
|
||||||
|
_client.ShardId)
|
||||||
|
&& x.ExpiresAt <= now)
|
||||||
|
.DeleteWithOutputAsync();
|
||||||
|
|
||||||
|
foreach (var d in deleted)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RemoveRole(d);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Log.Warning("Unable to remove temp role {RoleId} from user {UserId}",
|
||||||
|
d.RoleId,
|
||||||
|
d.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Unexpected error occurred in temprole loop");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveRole(TempRole tempRole)
|
||||||
|
{
|
||||||
|
var guild = _client.GetGuild(tempRole.GuildId);
|
||||||
|
|
||||||
|
var role = guild?.GetRole(tempRole.RoleId);
|
||||||
|
if (role is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var user = guild?.GetUser(tempRole.UserId);
|
||||||
|
if (user is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await user.RemoveRoleAsync(role);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,71 +1,32 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using Microsoft.EntityFrameworkCore;
|
using 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public interface IBotActivityService
|
||||||
|
{
|
||||||
|
Task SetActivityAsync(string game, ActivityType? type);
|
||||||
|
Task SetStreamAsync(string name, string link);
|
||||||
|
bool ToggleRotatePlaying();
|
||||||
|
Task AddPlaying(ActivityType statusType, string status);
|
||||||
|
Task<string> RemovePlayingAsync(int index);
|
||||||
|
IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses();
|
||||||
|
}
|
@@ -24,19 +24,22 @@ public partial class Administration
|
|||||||
private readonly IMedusaLoaderService _medusaLoader;
|
private readonly 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
||||||
|
@@ -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))
|
||||||
{
|
{
|
||||||
@@ -192,15 +192,22 @@ public partial class Administration
|
|||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageRoles)]
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
public async Task SarRemove([Leftover] IRole role)
|
[Priority(1)]
|
||||||
{
|
public Task SarRemove([Leftover] IRole role)
|
||||||
var guser = (IGuildUser)ctx.User;
|
=> SarRemove(role.Id);
|
||||||
|
|
||||||
var success = await _service.RemoveAsync(role.Guild.Id, role.Id);
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageRoles)]
|
||||||
|
[Priority(0)]
|
||||||
|
public async Task SarRemove([Leftover] ulong roleId)
|
||||||
|
{
|
||||||
|
var role = await ctx.Guild.GetRoleAsync(roleId);
|
||||||
|
var success = await _service.RemoveAsync(ctx.Guild.Id, roleId);
|
||||||
if (!success)
|
if (!success)
|
||||||
await Response().Error(strs.self_assign_not).SendAsync();
|
await Response().Error(strs.self_assign_not).SendAsync();
|
||||||
else
|
else
|
||||||
await Response().Confirm(strs.self_assign_rem(Format.Bold(role.Name))).SendAsync();
|
await Response().Confirm(strs.self_assign_rem(Format.Bold(role?.Name ?? roleId.ToString()))).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@@ -59,10 +59,15 @@ public class SelfAssignedRolesService : INService, IReadyExecutor
|
|||||||
},
|
},
|
||||||
_ => new()
|
_ => new()
|
||||||
{
|
{
|
||||||
|
SarGroupId = ctx.GetTable<SarGroup>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.First()
|
||||||
},
|
},
|
||||||
() => new()
|
() => new()
|
||||||
{
|
{
|
||||||
RoleId = roleId,
|
RoleId = roleId,
|
||||||
|
GuildId = guildId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +285,11 @@ public sealed class SarAssignerService : INService, IReadyExecutor
|
|||||||
|
|
||||||
if (item.Group.IsExclusive)
|
if (item.Group.IsExclusive)
|
||||||
{
|
{
|
||||||
var rolesToRemove = item.Group.Roles.Select(x => x.RoleId);
|
var rolesToRemove = item.Group.Roles
|
||||||
|
.Where(x => item.User.RoleIds.Contains(x.RoleId))
|
||||||
|
.Select(x => x.RoleId)
|
||||||
|
.ToArray();
|
||||||
|
if (rolesToRemove.Length > 0)
|
||||||
await item.User.RemoveRolesAsync(rolesToRemove);
|
await item.User.RemoveRolesAsync(rolesToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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]
|
||||||
@@ -115,6 +122,79 @@ 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()
|
||||||
{
|
{
|
||||||
|
@@ -13,6 +13,7 @@ using System.Globalization;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using NadekoBot.Modules.Gambling.Rps;
|
using NadekoBot.Modules.Gambling.Rps;
|
||||||
using NadekoBot.Common.TypeReaders;
|
using NadekoBot.Common.TypeReaders;
|
||||||
|
using NadekoBot.Modules.Games;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
using SixLabors.Fonts;
|
using SixLabors.Fonts;
|
||||||
using SixLabors.Fonts.Unicode;
|
using SixLabors.Fonts.Unicode;
|
||||||
@@ -40,6 +41,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
private readonly RakebackService _rb;
|
private readonly RakebackService _rb;
|
||||||
private readonly IBotCache _cache;
|
private readonly IBotCache _cache;
|
||||||
|
private readonly CaptchaService _captchaService;
|
||||||
|
|
||||||
public Gambling(
|
public Gambling(
|
||||||
IGamblingService gs,
|
IGamblingService gs,
|
||||||
@@ -54,7 +56,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
IPatronageService patronage,
|
IPatronageService patronage,
|
||||||
GamblingTxTracker gamblingTxTracker,
|
GamblingTxTracker gamblingTxTracker,
|
||||||
RakebackService rb,
|
RakebackService rb,
|
||||||
IBotCache cache)
|
IBotCache cache,
|
||||||
|
CaptchaService captchaService)
|
||||||
: base(configService)
|
: base(configService)
|
||||||
{
|
{
|
||||||
_gs = gs;
|
_gs = gs;
|
||||||
@@ -66,6 +69,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
_gamblingTxTracker = gamblingTxTracker;
|
_gamblingTxTracker = gamblingTxTracker;
|
||||||
_rb = rb;
|
_rb = rb;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
_captchaService = captchaService;
|
||||||
_ps = patronage;
|
_ps = patronage;
|
||||||
_rng = new NadekoRandom();
|
_rng = new NadekoRandom();
|
||||||
|
|
||||||
@@ -135,7 +139,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;
|
||||||
@@ -155,12 +158,22 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
}
|
}
|
||||||
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||||
{
|
{
|
||||||
var password = await GetUserTimelyPassword(ctx.User.Id);
|
var password = await _captchaService.GetUserCaptcha(ctx.User.Id);
|
||||||
|
|
||||||
|
if (password is not null)
|
||||||
|
{
|
||||||
var img = GetPasswordImage(password);
|
var img = GetPasswordImage(password);
|
||||||
using var stream = await img.ToStreamAsync();
|
await using var stream = await img.ToStreamAsync();
|
||||||
var captcha = await Response()
|
var toSend = Response()
|
||||||
.File(stream, "timely.png")
|
.File(stream, "timely.png");
|
||||||
.SendAsync();
|
|
||||||
|
#if GLOBAL_NADEKO
|
||||||
|
if (_rng.Next(0, 5) == 0)
|
||||||
|
toSend = toSend
|
||||||
|
.Confirm("[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var captchaMessage = await toSend.SendAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
@@ -169,35 +182,18 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ClearUserTimelyPassword(ctx.User.Id);
|
await _captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_ = captcha.DeleteAsync();
|
_ = captchaMessage.DeleteAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ClaimTimely();
|
await ClaimTimely();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TypedKey<string> TimelyPasswordKey(ulong userId)
|
|
||||||
=> new($"timely_password:{userId}");
|
|
||||||
|
|
||||||
private async Task<string> GetUserTimelyPassword(ulong userId)
|
|
||||||
{
|
|
||||||
var pw = await _cache.GetOrAddAsync(TimelyPasswordKey(userId),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var password = _service.GeneratePassword();
|
|
||||||
return Task.FromResult(password);
|
|
||||||
});
|
|
||||||
|
|
||||||
return pw;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueTask<bool> ClearUserTimelyPassword(ulong userId)
|
|
||||||
=> _cache.RemoveAsync(TimelyPasswordKey(userId));
|
|
||||||
|
|
||||||
private Image<Rgba32> GetPasswordImage(string password)
|
private Image<Rgba32> GetPasswordImage(string password)
|
||||||
{
|
{
|
||||||
var img = new Image<Rgba32>(50, 24);
|
var img = new Image<Rgba32>(50, 24);
|
||||||
@@ -872,9 +868,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 +883,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -16,9 +16,9 @@ public class GamblingCleanupService : IGamblingCleanupService, INService
|
|||||||
public async Task DeleteWaifus()
|
public async Task DeleteWaifus()
|
||||||
{
|
{
|
||||||
await using var ctx = _db.GetDbContext();
|
await using var ctx = _db.GetDbContext();
|
||||||
await ctx.GetTable<WaifuInfo>().DeleteAsync();
|
|
||||||
await ctx.GetTable<WaifuItem>().DeleteAsync();
|
await ctx.GetTable<WaifuItem>().DeleteAsync();
|
||||||
await ctx.GetTable<WaifuUpdate>().DeleteAsync();
|
await ctx.GetTable<WaifuUpdate>().DeleteAsync();
|
||||||
|
await ctx.GetTable<WaifuInfo>().DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteWaifu(ulong userId)
|
public async Task DeleteWaifu(ulong userId)
|
||||||
|
80
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
80
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Patronage;
|
||||||
|
using SixLabors.Fonts;
|
||||||
|
using SixLabors.Fonts.Unicode;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
|
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class CaptchaService(FontProvider fonts, IBotCache cache, IPatronageService ps) : INService
|
||||||
|
{
|
||||||
|
private readonly NadekoRandom _rng = new();
|
||||||
|
|
||||||
|
public Image<Rgba32> GetPasswordImage(string password)
|
||||||
|
{
|
||||||
|
var img = new Image<Rgba32>(50, 24);
|
||||||
|
|
||||||
|
var font = fonts.NotoSans.CreateFont(22);
|
||||||
|
var outlinePen = new SolidPen(Color.Black, 0.5f);
|
||||||
|
var strikeoutRun = new RichTextRun
|
||||||
|
{
|
||||||
|
Start = 0,
|
||||||
|
End = password.GetGraphemeCount(),
|
||||||
|
Font = font,
|
||||||
|
StrikeoutPen = new SolidPen(Color.White, 4),
|
||||||
|
TextDecorations = TextDecorations.Strikeout
|
||||||
|
};
|
||||||
|
|
||||||
|
// draw password on the image
|
||||||
|
img.Mutate(x =>
|
||||||
|
{
|
||||||
|
DrawTextExtensions.DrawText(x,
|
||||||
|
new RichTextOptions(font)
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
FallbackFontFamilies = fonts.FallBackFonts,
|
||||||
|
Origin = new(25, 12),
|
||||||
|
TextRuns = [strikeoutRun]
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
Brushes.Solid(Color.White),
|
||||||
|
outlinePen);
|
||||||
|
});
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GeneratePassword()
|
||||||
|
{
|
||||||
|
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
|
||||||
|
return new kwum(num).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypedKey<string> CaptchaPasswordKey(ulong userId)
|
||||||
|
=> new($"timely_password:{userId}");
|
||||||
|
|
||||||
|
public async Task<string?> GetUserCaptcha(ulong userId)
|
||||||
|
{
|
||||||
|
var patron = await ps.GetPatronAsync(userId);
|
||||||
|
if (patron is Patron p && !p.ValidThru.IsBeforeToday())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var pw = await cache.GetOrAddAsync(CaptchaPasswordKey(userId),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var password = GeneratePassword();
|
||||||
|
return Task.FromResult(password)!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return pw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> ClearUserCaptcha(ulong userId)
|
||||||
|
=> cache.RemoveAsync(CaptchaPasswordKey(userId));
|
||||||
|
}
|
27
src/NadekoBot/Modules/Games/Fish/FishCatch.cs
Normal file
27
src/NadekoBot/Modules/Games/Fish/FishCatch.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishCatch
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
public ulong UserId { get; set; }
|
||||||
|
public int FishId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public int MaxStars { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FishCatchConfiguration : IEntityTypeConfiguration<FishCatch>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<FishCatch> builder)
|
||||||
|
{
|
||||||
|
builder.HasAlternateKey(x => new
|
||||||
|
{
|
||||||
|
x.UserId,
|
||||||
|
x.FishId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
8
src/NadekoBot/Modules/Games/Fish/FishChance.cs
Normal file
8
src/NadekoBot/Modules/Games/Fish/FishChance.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishChance
|
||||||
|
{
|
||||||
|
public int Fish { get; set; } = 75;
|
||||||
|
public int Trash { get; set; } = 20;
|
||||||
|
public int Nothing { get; set; } = 0;
|
||||||
|
}
|
275
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
275
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
using Format = Discord.Format;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public partial class Games
|
||||||
|
{
|
||||||
|
public class FishCommands(
|
||||||
|
FishService fs,
|
||||||
|
FishConfigService fcs,
|
||||||
|
IBotCache cache,
|
||||||
|
CaptchaService captchaService) : NadekoModule
|
||||||
|
{
|
||||||
|
private static readonly NadekoRandom _rng = new();
|
||||||
|
|
||||||
|
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||||
|
=> new($"fishingwhitelist:{userId}");
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Fish()
|
||||||
|
{
|
||||||
|
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||||
|
if (cRes.TryPickT1(out _, out _))
|
||||||
|
{
|
||||||
|
var password = await captchaService.GetUserCaptcha(ctx.User.Id);
|
||||||
|
if (password is not null)
|
||||||
|
{
|
||||||
|
var img = captchaService.GetPasswordImage(password);
|
||||||
|
using var stream = await img.ToStreamAsync();
|
||||||
|
|
||||||
|
var toSend = Response()
|
||||||
|
.File(stream, "timely.png");
|
||||||
|
|
||||||
|
#if GLOBAL_NADEKO
|
||||||
|
if (_rng.Next(0, 5) == 0)
|
||||||
|
toSend = toSend
|
||||||
|
.Confirm("[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha.")
|
||||||
|
#endif
|
||||||
|
var captcha = await toSend.SendAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
|
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelist the user for 30 minutes
|
||||||
|
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||||
|
// reset the password
|
||||||
|
await captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ = captcha.DeleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
|
if (fishResult.TryPickT1(out _, out var fishTask))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWeather = fs.GetCurrentWeather();
|
||||||
|
var currentTod = fs.GetTime();
|
||||||
|
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||||
|
|
||||||
|
var msg = await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithPendingColor()
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithDescription(GetText(strs.fish_waiting))
|
||||||
|
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
|
||||||
|
.AddField(GetText(strs.fish_weather),
|
||||||
|
GetWeatherEmoji(currentWeather) + " " + currentWeather,
|
||||||
|
true)
|
||||||
|
.AddField(GetText(strs.fish_tod), GetTodEmoji(currentTod) + " " + currentTod, true))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
var res = await fishTask;
|
||||||
|
if (res is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.fish_nothing).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var desc = GetText(strs.fish_caught(res.Fish.Emoji + " " + Format.Bold(res.Fish.Name)));
|
||||||
|
|
||||||
|
if (res.IsSkillUp)
|
||||||
|
{
|
||||||
|
desc += "\n" + GetText(strs.fish_skill_up(res.Skill, res.MaxSkill));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithDescription(desc)
|
||||||
|
.AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
|
||||||
|
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||||
|
.WithThumbnailUrl(res.Fish.Image))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
await msg.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task FishSpot()
|
||||||
|
{
|
||||||
|
var ws = fs.GetWeatherForPeriods(7);
|
||||||
|
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||||
|
var time = fs.GetTime();
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(GetText(strs.fish_weather_duration(fs.GetWeatherPeriodDuration())))
|
||||||
|
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot, true)
|
||||||
|
.AddField(GetText(strs.fish_tod), GetTodEmoji(time) + " " + time, true)
|
||||||
|
.AddField(GetText(strs.fish_weather_forecast),
|
||||||
|
ws.Select(x => GetWeatherEmoji(x)).Join(""),
|
||||||
|
true))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Fishlist(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fishes = await fs.GetAllFish();
|
||||||
|
|
||||||
|
var catches = await fs.GetUserCatches(ctx.User.Id);
|
||||||
|
var (skill, maxSkill) = await fs.GetSkill(ctx.User.Id);
|
||||||
|
|
||||||
|
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(fishes)
|
||||||
|
.PageSize(9)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((fs, i) =>
|
||||||
|
{
|
||||||
|
var eb = CreateEmbed()
|
||||||
|
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithTitle(GetText(strs.fish_list_title))
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
foreach (var f in fs)
|
||||||
|
{
|
||||||
|
if (catchDict.TryGetValue(f.Id, out var c))
|
||||||
|
{
|
||||||
|
eb.AddField(f.Name,
|
||||||
|
GetFishEmoji(f, c.Count)
|
||||||
|
+ " "
|
||||||
|
+ GetSpotEmoji(f.Spot)
|
||||||
|
+ GetTodEmoji(f.Time)
|
||||||
|
+ GetWeatherEmoji(f.Weather)
|
||||||
|
+ "\n"
|
||||||
|
+ GetStarText(c.MaxStars, f.Stars)
|
||||||
|
+ "\n"
|
||||||
|
+ Format.Italics(f.Fluff),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + GetStarText(0, f.Stars), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eb;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFishEmoji(FishData? fish, int count)
|
||||||
|
{
|
||||||
|
if (fish is null)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return fish.Emoji + " x" + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSpotEmoji(FishingSpot? spot)
|
||||||
|
{
|
||||||
|
if (spot is not FishingSpot fs)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var conf = fcs.Data;
|
||||||
|
|
||||||
|
return conf.SpotEmojis[(int)fs];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTodEmoji(FishingTime? fishTod)
|
||||||
|
{
|
||||||
|
return fishTod switch
|
||||||
|
{
|
||||||
|
FishingTime.Night => "🌑",
|
||||||
|
FishingTime.Dawn => "🌅",
|
||||||
|
FishingTime.Dusk => "🌆",
|
||||||
|
FishingTime.Day => "☀️",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetWeatherEmoji(FishingWeather? w)
|
||||||
|
=> w switch
|
||||||
|
{
|
||||||
|
FishingWeather.Rain => "🌧️",
|
||||||
|
FishingWeather.Snow => "❄️",
|
||||||
|
FishingWeather.Storm => "⛈️",
|
||||||
|
FishingWeather.Clear => "☀️",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetStarText(int resStars, int fishStars)
|
||||||
|
{
|
||||||
|
if (resStars == fishStars)
|
||||||
|
{
|
||||||
|
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = fcs.Data;
|
||||||
|
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
|
||||||
|
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
|
||||||
|
|
||||||
|
return starsp1 + starsp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MultiplyStars(string starEmoji, int count)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sb.Append(starEmoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingSpot
|
||||||
|
{
|
||||||
|
Ocean,
|
||||||
|
River,
|
||||||
|
Lake,
|
||||||
|
Swamp,
|
||||||
|
Reef
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingTime
|
||||||
|
{
|
||||||
|
Night,
|
||||||
|
Dawn,
|
||||||
|
Day,
|
||||||
|
Dusk
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingWeather
|
||||||
|
{
|
||||||
|
Clear,
|
||||||
|
Rain,
|
||||||
|
Storm,
|
||||||
|
Snow
|
||||||
|
}
|
19
src/NadekoBot/Modules/Games/Fish/FishConfig.cs
Normal file
19
src/NadekoBot/Modules/Games/Fish/FishConfig.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Cloneable;
|
||||||
|
using NadekoBot.Common.Yml;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
[Cloneable]
|
||||||
|
public sealed partial class FishConfig : ICloneable<FishConfig>
|
||||||
|
{
|
||||||
|
[Comment("DO NOT CHANGE")]
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public string WeatherSeed { get; set; } = string.Empty;
|
||||||
|
public List<string> StarEmojis { get; set; } = new();
|
||||||
|
public List<string> SpotEmojis { get; set; } = new();
|
||||||
|
public FishChance Chance { get; set; } = new FishChance();
|
||||||
|
|
||||||
|
public List<FishData> Fish { get; set; } = new();
|
||||||
|
public List<FishData> Trash { get; set; } = new();
|
||||||
|
}
|
19
src/NadekoBot/Modules/Games/Fish/FishConfigService.cs
Normal file
19
src/NadekoBot/Modules/Games/Fish/FishConfigService.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using NadekoBot.Common.Configs;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishConfigService : ConfigServiceBase<FishConfig>
|
||||||
|
{
|
||||||
|
private static string FILE_PATH = "data/fish.yml";
|
||||||
|
private static readonly TypedKey<FishConfig> _changeKey = new("config.fish.updated");
|
||||||
|
|
||||||
|
public override string Name
|
||||||
|
=> "fishing";
|
||||||
|
|
||||||
|
public FishConfigService(
|
||||||
|
IConfigSeria serializer,
|
||||||
|
IPubSub pubSub)
|
||||||
|
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
16
src/NadekoBot/Modules/Games/Fish/FishData.cs
Normal file
16
src/NadekoBot/Modules/Games/Fish/FishData.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public class FishData
|
||||||
|
{
|
||||||
|
public required int Id { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public FishingWeather? Weather { get; set; }
|
||||||
|
public FishingSpot? Spot { get; set; }
|
||||||
|
public FishingTime? Time { get; set; }
|
||||||
|
public required double Chance { get; set; }
|
||||||
|
public required int Stars { get; set; }
|
||||||
|
public required string Fluff { get; set; }
|
||||||
|
public List<string>? Condition { get; set; }
|
||||||
|
public string? Image { get; init; }
|
||||||
|
public string? Emoji { get; set; }
|
||||||
|
}
|
12
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
12
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishResult
|
||||||
|
{
|
||||||
|
public required FishData Fish { get; init; }
|
||||||
|
public int Stars { get; init; }
|
||||||
|
public bool IsSkillUp { get; set; }
|
||||||
|
public int Skill { get; set; }
|
||||||
|
public int MaxSkill { get; set; }
|
||||||
|
}
|
||||||
|
public readonly record struct AlreadyFishing;
|
||||||
|
|
421
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
421
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : INService
|
||||||
|
{
|
||||||
|
public const double MAX_SKILL = 100;
|
||||||
|
|
||||||
|
private Random _rng = new Random();
|
||||||
|
|
||||||
|
private static TypedKey<bool> FishingKey(ulong userId)
|
||||||
|
=> new($"fishing:{userId}");
|
||||||
|
|
||||||
|
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
|
||||||
|
{
|
||||||
|
var duration = _rng.Next(5, 9);
|
||||||
|
|
||||||
|
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
|
||||||
|
{
|
||||||
|
return new AlreadyFishing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryFishAsync(userId, channelId, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FishResult?> TryFishAsync(ulong userId, ulong channelId, int duration)
|
||||||
|
{
|
||||||
|
var conf = fcs.Data;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(duration));
|
||||||
|
|
||||||
|
var (playerSkill, _) = await GetSkill(userId);
|
||||||
|
var fishChanceMultiplier = Math.Clamp((playerSkill + 20) / MAX_SKILL, 0, 1);
|
||||||
|
var trashChanceMultiplier = Math.Clamp(((2 * MAX_SKILL) - playerSkill) / MAX_SKILL, 1, 2);
|
||||||
|
|
||||||
|
var nothingChance = conf.Chance.Nothing;
|
||||||
|
var fishChance = conf.Chance.Fish * fishChanceMultiplier;
|
||||||
|
var trashChance = conf.Chance.Trash * trashChanceMultiplier;
|
||||||
|
|
||||||
|
// first roll whether it's fish, trash or nothing
|
||||||
|
var totalChance = fishChance + trashChance + conf.Chance.Nothing;
|
||||||
|
|
||||||
|
var typeRoll = _rng.NextDouble() * totalChance;
|
||||||
|
|
||||||
|
if (typeRoll < nothingChance)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = typeRoll < nothingChance + fishChance
|
||||||
|
? conf.Fish
|
||||||
|
: conf.Trash;
|
||||||
|
|
||||||
|
|
||||||
|
var result = await FishAsyncInternal(userId, channelId, items);
|
||||||
|
|
||||||
|
if (result is not null)
|
||||||
|
{
|
||||||
|
var isSkillUp = await TrySkillUpAsync(userId, playerSkill);
|
||||||
|
|
||||||
|
result.IsSkillUp = isSkillUp;
|
||||||
|
result.MaxSkill = (int)MAX_SKILL;
|
||||||
|
result.Skill = playerSkill;
|
||||||
|
|
||||||
|
if (isSkillUp)
|
||||||
|
{
|
||||||
|
result.Skill += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TrySkillUpAsync(ulong userId, int playerSkill)
|
||||||
|
{
|
||||||
|
var skillUpProb = GetSkillUpProb(playerSkill);
|
||||||
|
|
||||||
|
var rng = _rng.NextDouble();
|
||||||
|
|
||||||
|
if (rng < skillUpProb)
|
||||||
|
{
|
||||||
|
await using var ctx = db.GetDbContext();
|
||||||
|
|
||||||
|
var maxSkill = (int)MAX_SKILL;
|
||||||
|
await ctx.GetTable<UserFishStats>()
|
||||||
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Skill = 1,
|
||||||
|
},
|
||||||
|
(old) => new()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Skill = playerSkill
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetSkillUpProb(int playerSkill)
|
||||||
|
{
|
||||||
|
if (playerSkill < 0)
|
||||||
|
playerSkill = 0;
|
||||||
|
|
||||||
|
if (playerSkill >= 100)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return 1 / (Math.Pow(Math.E, playerSkill / 22d));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(int skill, int maxSkill)> GetSkill(ulong userId)
|
||||||
|
{
|
||||||
|
await using var ctx = db.GetDbContext();
|
||||||
|
|
||||||
|
var skill = await ctx.GetTable<UserFishStats>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Select(x => x.Skill)
|
||||||
|
.FirstOrDefaultAsyncLinqToDB();
|
||||||
|
|
||||||
|
return (skill, (int)MAX_SKILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
|
||||||
|
{
|
||||||
|
var filteredItems = new List<FishData>();
|
||||||
|
|
||||||
|
var loc = GetSpot(channelId);
|
||||||
|
var time = GetTime();
|
||||||
|
var w = GetWeather(DateTime.UtcNow);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Condition is { Count: > 0 })
|
||||||
|
{
|
||||||
|
if (!item.Condition.Any(x => channelId.ToString().EndsWith(x)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Spot is not null && item.Spot != loc)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.Time is not null && item.Time != time)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.Weather is not null && item.Weather != w)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
filteredItems.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxSum = filteredItems.Sum(x => x.Chance * 100);
|
||||||
|
|
||||||
|
|
||||||
|
var roll = _rng.NextDouble() * maxSum;
|
||||||
|
|
||||||
|
FishResult? caught = null;
|
||||||
|
|
||||||
|
var curSum = 0d;
|
||||||
|
foreach (var i in filteredItems)
|
||||||
|
{
|
||||||
|
curSum += i.Chance * 100;
|
||||||
|
|
||||||
|
if (roll < curSum)
|
||||||
|
{
|
||||||
|
caught = new FishResult()
|
||||||
|
{
|
||||||
|
Fish = i,
|
||||||
|
Stars = GetRandomStars(i.Stars),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caught is not null)
|
||||||
|
{
|
||||||
|
await using var uow = db.GetDbContext();
|
||||||
|
|
||||||
|
await uow.GetTable<FishCatch>()
|
||||||
|
.InsertOrUpdateAsync(() => new FishCatch()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
FishId = caught.Fish.Id,
|
||||||
|
MaxStars = caught.Stars,
|
||||||
|
Count = 1
|
||||||
|
},
|
||||||
|
(old) => new()
|
||||||
|
{
|
||||||
|
Count = old.Count + 1,
|
||||||
|
MaxStars = Math.Max((int)old.MaxStars, caught.Stars),
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
FishId = caught.Fish.Id,
|
||||||
|
UserId = userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return caught;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Error(
|
||||||
|
"Something went wrong in the fish command, no fish with sufficient chance was found, Roll: {Roll}, MaxSum: {MaxSum}",
|
||||||
|
roll,
|
||||||
|
maxSum);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingSpot GetSpot(ulong channelId)
|
||||||
|
{
|
||||||
|
var cid = (channelId >> 22 >> 29) % 10;
|
||||||
|
|
||||||
|
return cid switch
|
||||||
|
{
|
||||||
|
< 1 => FishingSpot.Reef,
|
||||||
|
< 3 => FishingSpot.River,
|
||||||
|
< 5 => FishingSpot.Lake,
|
||||||
|
< 7 => FishingSpot.Swamp,
|
||||||
|
_ => FishingSpot.Ocean,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingTime GetTime()
|
||||||
|
{
|
||||||
|
var hour = DateTime.UtcNow.Hour % 12;
|
||||||
|
|
||||||
|
if (hour < 3)
|
||||||
|
return FishingTime.Night;
|
||||||
|
|
||||||
|
if (hour < 4)
|
||||||
|
return FishingTime.Dawn;
|
||||||
|
|
||||||
|
if (hour < 11)
|
||||||
|
return FishingTime.Day;
|
||||||
|
|
||||||
|
return FishingTime.Dusk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int WEATHER_PERIODS_PER_DAY = 12;
|
||||||
|
|
||||||
|
public IReadOnlyList<FishingWeather> GetWeatherForPeriods(int periods)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var result = new FishingWeather[periods];
|
||||||
|
|
||||||
|
for (var i = 0; i < periods; i++)
|
||||||
|
{
|
||||||
|
result[i] = GetWeather(now.AddHours(i * GetWeatherPeriodDuration()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingWeather GetCurrentWeather()
|
||||||
|
=> GetWeather(DateTime.UtcNow);
|
||||||
|
|
||||||
|
public FishingWeather GetWeather(DateTime time)
|
||||||
|
=> GetWeather(time, fcs.Data.WeatherSeed);
|
||||||
|
|
||||||
|
private FishingWeather GetWeather(DateTime time, string seed)
|
||||||
|
{
|
||||||
|
var year = time.Year;
|
||||||
|
var dayOfYear = time.DayOfYear;
|
||||||
|
var hour = time.Hour;
|
||||||
|
|
||||||
|
var num = (year * 100_000) + (dayOfYear * 100) + (hour / GetWeatherPeriodDuration());
|
||||||
|
|
||||||
|
Span<byte> dataArray = stackalloc byte[4];
|
||||||
|
BitConverter.TryWriteBytes(dataArray, num);
|
||||||
|
|
||||||
|
Span<byte> seedArray = stackalloc byte[seed.Length];
|
||||||
|
for (var index = 0; index < seed.Length; index++)
|
||||||
|
{
|
||||||
|
var c = seed[index];
|
||||||
|
seedArray[index] = (byte)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> arr = stackalloc byte[dataArray.Length + seedArray.Length];
|
||||||
|
|
||||||
|
dataArray.CopyTo(arr);
|
||||||
|
seedArray.CopyTo(arr[dataArray.Length..]);
|
||||||
|
|
||||||
|
using var algo = SHA512.Create();
|
||||||
|
|
||||||
|
Span<byte> hash = stackalloc byte[64];
|
||||||
|
algo.TryComputeHash(arr, hash, out _);
|
||||||
|
|
||||||
|
byte reduced = 0;
|
||||||
|
foreach (var u in hash)
|
||||||
|
reduced ^= u;
|
||||||
|
|
||||||
|
var r = reduced % 16;
|
||||||
|
|
||||||
|
// return (FishingWeather)r;
|
||||||
|
return r switch
|
||||||
|
{
|
||||||
|
< 5 => FishingWeather.Clear,
|
||||||
|
< 9 => FishingWeather.Rain,
|
||||||
|
< 13 => FishingWeather.Storm,
|
||||||
|
_ => FishingWeather.Snow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random number of stars between 1 and maxStars
|
||||||
|
/// if maxStars == 1, returns 1
|
||||||
|
/// if maxStars == 2, returns 1 (66%) or 2 (33%)
|
||||||
|
/// if maxStars == 3, returns 1 (65%) or 2 (25%) or 3 (10%)
|
||||||
|
/// if maxStars == 5, returns 1 (40%) or 2 (30%) or 3 (15%) or 4 (10%) or 5 (5%)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxStars">Max Number of stars to generate</param>
|
||||||
|
/// <returns>Random number of stars</returns>
|
||||||
|
private int GetRandomStars(int maxStars)
|
||||||
|
{
|
||||||
|
if (maxStars == 1)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (maxStars == 2)
|
||||||
|
{
|
||||||
|
// 15% chance of 1 star, 85% chance of 2 stars
|
||||||
|
return _rng.NextDouble() < 0.85 ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 3)
|
||||||
|
{
|
||||||
|
// 65% chance of 1 star, 30% chance of 2 stars, 5% chance of 3 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.65)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.95)
|
||||||
|
return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 4)
|
||||||
|
{
|
||||||
|
// this should never happen
|
||||||
|
// 50% chance of 1 star, 25% chance of 2 stars, 18% chance of 3 stars, 7% chance of 4 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.55)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.80)
|
||||||
|
return 2;
|
||||||
|
if (r < 0.98)
|
||||||
|
return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 5)
|
||||||
|
{
|
||||||
|
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 5% chance of 5 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.4)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.7)
|
||||||
|
return 2;
|
||||||
|
if (r < 0.9)
|
||||||
|
return 3;
|
||||||
|
if (r < 0.98)
|
||||||
|
return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetWeatherPeriodDuration()
|
||||||
|
=> 24 / WEATHER_PERIODS_PER_DAY;
|
||||||
|
|
||||||
|
public async Task<List<FishData>> GetAllFish()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
var conf = fcs.Data;
|
||||||
|
return conf.Fish.Concat(conf.Trash).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<FishCatch>> GetUserCatches(ulong userId)
|
||||||
|
{
|
||||||
|
await using var ctx = db.GetDbContext();
|
||||||
|
|
||||||
|
var catches = await ctx.GetTable<FishCatch>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
return catches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UserFishStats
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public ulong UserId { get; set; }
|
||||||
|
public int Skill { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UserFishStatsConfiguration : IEntityTypeConfiguration<UserFishStats>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserFishStats> builder)
|
||||||
|
{
|
||||||
|
builder.HasIndex(x => x.UserId)
|
||||||
|
.IsUnique();
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithDescription(ctx.User.ToString())
|
.WithDescription(ctx.User.ToString())
|
||||||
.AddField("❓ " + GetText(strs.question), question)
|
.AddField("❓ " + GetText(strs.question), question)
|
||||||
.AddField("🎱 " + GetText(strs._8ball), res)).SendAsync();
|
.AddField("🎱 " + GetText(strs._8ball), res))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly string[] _numberEmojis = ["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"];
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Minesweeper(int numberOfMines = 12)
|
||||||
|
{
|
||||||
|
var boardSizeX = 9;
|
||||||
|
var boardSizeY = 10;
|
||||||
|
|
||||||
|
if (numberOfMines < 1)
|
||||||
|
{
|
||||||
|
numberOfMines = 1;
|
||||||
|
}
|
||||||
|
else if (numberOfMines > boardSizeX * boardSizeY / 2)
|
||||||
|
{
|
||||||
|
numberOfMines = boardSizeX * boardSizeY / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mineIndicies = Enumerable.Range(0, boardSizeX * boardSizeY)
|
||||||
|
.ToArray()
|
||||||
|
.Shuffle()
|
||||||
|
.Take(numberOfMines)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
string GetNumberOnCell(int x, int y)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
for (var i = -1; i < 2; i++)
|
||||||
|
{
|
||||||
|
for (var j = -1; j < 2; j++)
|
||||||
|
{
|
||||||
|
if (y + j >= boardSizeY || y + j < 0)
|
||||||
|
continue;
|
||||||
|
if (x + i >= boardSizeX || x + i < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var boardIndex = (y + j) * boardSizeX + (x + i);
|
||||||
|
if (mineIndicies.Contains(boardIndex))
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _numberEmojis[count];
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"### Minesweeper [{numberOfMines}\\💣]");
|
||||||
|
for (var i = 0; i < boardSizeY; i++)
|
||||||
|
{
|
||||||
|
for (var j = 0; j < boardSizeX; j++)
|
||||||
|
{
|
||||||
|
var emoji = mineIndicies.Contains((i * boardSizeX) + j) ? "💣" : GetNumberOnCell(j, i);
|
||||||
|
sb.Append($"||{emoji}||");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response().Text(sb.ToString()).SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -65,7 +65,17 @@ public sealed class MusicPlayer : IMusicPlayer
|
|||||||
|
|
||||||
_songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
|
_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,13 +412,25 @@ public sealed class MusicPlayer : IMusicPlayer
|
|||||||
if (song is null)
|
if (song is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
int index;
|
|
||||||
|
|
||||||
|
var wasLast = _queue.IsLast();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int index;
|
||||||
if (asNext)
|
if (asNext)
|
||||||
return (_queue.EnqueueNext(song, queuer, out index), index);
|
return (_queue.EnqueueNext(song, queuer, out index), index);
|
||||||
|
|
||||||
return (_queue.Enqueue(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)
|
||||||
{
|
{
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -24,11 +24,20 @@ 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)
|
||||||
|
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))
|
||||||
return Task.FromResult(true);
|
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 (bs.Blocked.Modules.Add(moduleName))
|
if (priv)
|
||||||
|
{
|
||||||
|
if (bs.DmBlocked.Modules.Add(moduleName))
|
||||||
|
{
|
||||||
added = true;
|
added = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bs.DmBlocked.Modules.Remove(moduleName);
|
||||||
|
added = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bs.Blocked.Modules.Add(moduleName))
|
||||||
|
{
|
||||||
|
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 (bs.Blocked.Commands.Add(commandName))
|
if (priv)
|
||||||
|
{
|
||||||
|
if (bs.DmBlocked.Commands.Add(commandName))
|
||||||
|
{
|
||||||
added = true;
|
added = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bs.DmBlocked.Commands.Remove(commandName);
|
||||||
|
added = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bs.Blocked.Commands.Add(commandName))
|
||||||
|
{
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bs.Blocked.Commands.Remove(commandName);
|
bs.Blocked.Commands.Remove(commandName);
|
||||||
|
@@ -411,6 +411,34 @@ public partial class Searches : NadekoModule<SearchesService>
|
|||||||
.SendAsync();
|
.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();
|
||||||
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task Wikia(string target, [Leftover] string query)
|
public async Task Wikia(string target, [Leftover] string query)
|
||||||
{
|
{
|
||||||
|
@@ -122,13 +122,16 @@ 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));
|
||||||
|
|
||||||
var msg = await arg1.GetOrDownloadAsync();
|
var msg = await arg1.GetOrDownloadAsync();
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
@@ -119,6 +114,7 @@ public sealed class AfkService : INService, IReadyExecutor
|
|||||||
mentionedUserId = repliedUserId;
|
mentionedUserId = repliedUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _cache.GetAsync(GetKey(mentionedUserId));
|
var result = await _cache.GetAsync(GetKey(mentionedUserId));
|
||||||
@@ -128,13 +124,32 @@ public sealed class AfkService : INService, IReadyExecutor
|
|||||||
|
|
||||||
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)
|
||||||
|
@@ -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 + "\"";
|
||||||
|
@@ -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));
|
||||||
|
@@ -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
|
||||||
{
|
{
|
||||||
|
@@ -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()
|
||||||
|
@@ -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),
|
||||||
|
@@ -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()
|
||||||
{
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
return await uow.Set<Reminder>()
|
var earliest = await uow.Set<Reminder>()
|
||||||
.ToLinqToDBTable()
|
.ToLinqToDBTable()
|
||||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId, _creds.TotalShards, _client.ShardId)
|
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId,
|
||||||
&& x.When < now)
|
_creds.TotalShards,
|
||||||
.ToListAsyncLinqToDB();
|
_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,7 +272,10 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
|||||||
string message,
|
string message,
|
||||||
ReminderType reminderType)
|
ReminderType reminderType)
|
||||||
{
|
{
|
||||||
var rem = new Reminder
|
await using (var ctx = _db.GetDbContext())
|
||||||
|
{
|
||||||
|
await ctx.GetTable<Reminder>()
|
||||||
|
.InsertAsync(() => new Reminder
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
ChannelId = targetId,
|
ChannelId = targetId,
|
||||||
@@ -251,15 +283,15 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
|||||||
IsPrivate = isPrivate,
|
IsPrivate = isPrivate,
|
||||||
When = time,
|
When = time,
|
||||||
Message = message,
|
Message = message,
|
||||||
Type = reminderType
|
Type = reminderType,
|
||||||
};
|
DateAdded = DateTime.UtcNow
|
||||||
|
});
|
||||||
await using var ctx = _db.GetDbContext();
|
|
||||||
await ctx.Set<Reminder>()
|
|
||||||
.AddAsync(rem);
|
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_tcs.SetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Reminder>> GetServerReminders(int page, ulong guildId)
|
public async Task<List<Reminder>> GetServerReminders(int page, ulong guildId)
|
||||||
{
|
{
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
|
@@ -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)
|
||||||
|
@@ -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();
|
||||||
|
11
src/NadekoBot/Modules/Xp/BuyResult.cs
Normal file
11
src/NadekoBot/Modules/Xp/BuyResult.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace NadekoBot.Modules.Xp.Services;
|
||||||
|
|
||||||
|
public enum BuyResult
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
XpShopDisabled,
|
||||||
|
AlreadyOwned,
|
||||||
|
InsufficientFunds,
|
||||||
|
UnknownItem,
|
||||||
|
InsufficientPatronTier,
|
||||||
|
}
|
@@ -51,33 +51,6 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task XpNotify()
|
|
||||||
{
|
|
||||||
var globalSetting = _service.GetNotificationType(ctx.User);
|
|
||||||
var serverSetting = _service.GetNotificationType(ctx.User.Id, ctx.Guild.Id);
|
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
|
||||||
.WithOkColor()
|
|
||||||
.AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting))
|
|
||||||
.AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting));
|
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task XpNotify(NotifyPlace place, XpNotificationLocation type)
|
|
||||||
{
|
|
||||||
if (place == NotifyPlace.Guild)
|
|
||||||
await _service.ChangeNotificationType(ctx.User.Id, ctx.Guild.Id, type);
|
|
||||||
else
|
|
||||||
await _service.ChangeNotificationType(ctx.User, type);
|
|
||||||
|
|
||||||
await ctx.OkAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.Administrator)]
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
@@ -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}");
|
||||||
@@ -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)]
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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.0</Version>
|
<Version>5.3.6</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"/>
|
||||||
|
2
src/NadekoBot/NadekoBot.csproj.DotSettings
Normal file
2
src/NadekoBot/NadekoBot.csproj.DotSettings
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cadministration_005Cnotify_005Cmodels/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@@ -54,13 +54,9 @@ public sealed class NadekoRandom : Random
|
|||||||
{
|
{
|
||||||
var bytes = new byte[sizeof(double)];
|
var bytes = new byte[sizeof(double)];
|
||||||
_rng.GetBytes(bytes);
|
_rng.GetBytes(bytes);
|
||||||
return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1);
|
return Math.Abs((BitConverter.ToDouble(bytes, 0) / (double.MaxValue + 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double NextDouble()
|
public override double NextDouble()
|
||||||
{
|
=> Sample();
|
||||||
var bytes = new byte[sizeof(double)];
|
|
||||||
_rng.GetBytes(bytes);
|
|
||||||
return BitConverter.ToDouble(bytes, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -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,
|
||||||
}
|
}
|
@@ -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; }
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user