mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Compare commits
107 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7bff20cc70 | ||
|
29f5dcc359 | ||
|
14f2851072 | ||
|
a2b25f8246 | ||
|
a38951b5ad | ||
|
4c1b911cb7 | ||
|
6c9f231453 | ||
|
83daf3c30f | ||
|
9be8140d4d | ||
|
96c9b699aa | ||
|
3c0768a372 | ||
|
58b22e3d9e | ||
|
0474551e2f | ||
|
c85bdec396 | ||
|
5fa39eaa9f | ||
|
8eaaa35c7a | ||
|
1c24f95efa | ||
|
fcc49dbbdb | ||
|
3a317590b4 | ||
|
36c013fbb5 | ||
|
a9ec8049f6 | ||
|
ced0d97e3a | ||
|
24d0f57dc3 | ||
|
514ecd6be8 | ||
|
02eb6e172b | ||
|
d22c579875 | ||
|
f70e49fc6a | ||
|
8b410561f9 | ||
|
3c79fd1d6f | ||
|
e9f1a9b1dd | ||
|
3c293ae6db | ||
|
a5d9e7de66 | ||
|
4d9e48cd41 | ||
|
b7ead22e09 | ||
|
9f219cddbb | ||
|
cf9792f24a | ||
|
0187dd57ac | ||
|
2d2e54e31e | ||
|
cb7c5e48fd | ||
|
771c2745dc | ||
|
59c0f2f4b3 | ||
|
219ca39cd1 | ||
|
1e6d0806d7 | ||
|
71f1e43272 | ||
|
8499e1da70 | ||
|
a2ea806bed | ||
|
732b5dfeed | ||
|
d4dcdc761a | ||
|
57996ba290 | ||
|
4b29b3a239 | ||
|
54ac955395 | ||
|
f4fa298866 | ||
|
b2fafc964f | ||
|
22b452e449 | ||
|
fda385a5e4 | ||
|
c28f7cfa07 | ||
|
0a029a7847 | ||
|
c050ce2123 | ||
|
27613410dd | ||
|
1513008b4b | ||
|
bf97cffd84 | ||
|
e37d1c46db | ||
|
06c20c6fa4 | ||
|
aa518d60a5 | ||
|
d55ce7accc | ||
|
502c5cec07 | ||
|
ee5c13607b | ||
|
5a681a5194 | ||
|
68395372f0 | ||
|
c8e01bd158 | ||
|
1d57191700 | ||
|
02c7ded457 | ||
|
12c483d222 | ||
|
c80898a7bf | ||
|
aae2805785 | ||
|
fc3695d090 | ||
|
428429ff44 | ||
|
dc344caec6 | ||
|
2a4d55f81d | ||
|
d090aa23ee | ||
|
65062306c6 | ||
|
9ae3b66fc2 | ||
|
c4ba43ec6d | ||
|
1141791ce5 | ||
|
49f1ef7db0 | ||
|
a70c35e101 | ||
|
717543f6c2 | ||
|
b61b1dbfaa | ||
|
92365fd22d | ||
|
24a4745193 | ||
|
1af75fd813 | ||
|
18160164eb | ||
|
2fd7d97025 | ||
|
6ada15049d | ||
|
0ebc40b95c | ||
|
02de25a931 | ||
|
0b395e9176 | ||
|
4532f992cd | ||
|
34201f0558 | ||
|
d2f4d63183 | ||
|
b41c014869 | ||
|
d348347762 | ||
|
db7cf3d757 | ||
|
83ea046d5f | ||
|
d5c94424e9 | ||
|
ff95b3d00f | ||
|
4b5fa3bb04 |
89
CHANGELOG.md
89
CHANGELOG.md
@@ -4,6 +4,95 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
|
||||
|
||||
## Unreleased
|
||||
|
||||
-
|
||||
|
||||
## [3.0.11] - 17.12.2021
|
||||
|
||||
### Added
|
||||
- `.remindl` and `.remindrm` commands now supports optional 'server' parameter for Administrators which allows them to delete any reminder created on the server
|
||||
- Added slots.currencyFontColor to gambling.yml
|
||||
- Added `.qexport` and `.qimport` commands which allow you to export and import quotes just like `.crsexport`
|
||||
- Added `.showembed <msgid>` and `.showembed #channel <msgid>` which will show you embed json from the specified message
|
||||
|
||||
### Changed
|
||||
- `.at` and `.atl` commands reworked
|
||||
- Persist restarts
|
||||
- Will now only translate non-commands
|
||||
- You can switch between `.at del` and `.at` without clearing the user language registrations
|
||||
- Disabling `.at` will clear all user language registrations on that channel
|
||||
- Users can't register languages if the `.at` is not enabled
|
||||
- Looks much nicer
|
||||
- Bot will now reply to user messages with a translation if `del` is disabled
|
||||
- Bot will make an embed with original and translated text with user avatar and name if `del` is enabled
|
||||
- If the bot is unable to delete messages while having `del` enabled, it will reset back to the no-del behavior for the current session
|
||||
|
||||
### Fixed
|
||||
- `.crypto` now supports top 5000 coins
|
||||
|
||||
## [3.0.10] - 01.12.2021
|
||||
|
||||
### Changed
|
||||
- `.warn` now supports weighted warnings
|
||||
- `.warnlog` will now show current amount and total amount of warnings
|
||||
|
||||
### Fixed
|
||||
- `.xprewsreset` now has correct permissions
|
||||
|
||||
### Removed
|
||||
- Removed slot.numbers from `images.yml` as they're no longer used
|
||||
|
||||
## [3.0.9] - 21.11.2021
|
||||
|
||||
### Changed
|
||||
- `.ea` will now use an image attachments if you omit imageUrl
|
||||
|
||||
### Added
|
||||
- Added `.emojiadd` with 3 overloads
|
||||
- `.ea :customEmoji:` which copies another server's emoji
|
||||
- `.ea newName :customEmoji:` which copies emoji under a different name
|
||||
- `.ea emojiName <imagelink.png>` which creates a new emoji from the specified image
|
||||
- Patreon Access and Refresh Tokens should now be automatically updated once a month as long as the user has provided the necessary credentials in creds.yml file:
|
||||
- `Patreon.ClientId`
|
||||
- `Patreon.RefreshToken` (will also get updated once a month but needs an initial value)
|
||||
- `Patreon.ClientSecret`
|
||||
- `Patreon.CampaignId`
|
||||
|
||||
### Fixed
|
||||
- Fixed an error that would show up in the console when a club image couldn't be drawn in certain circumstances
|
||||
|
||||
## [3.0.8] - 03.11.2021
|
||||
|
||||
### Added
|
||||
- Created VotesApi project nad re-worked vote rewards handling
|
||||
- Updated votes entries in creds.yml with explanations on how to set up vote links
|
||||
|
||||
### Fixed
|
||||
- Fixed adding currency to users who don't exist in the database
|
||||
- Memory used by the bot is now correct (thanks to kotz)
|
||||
- Ban/kick will no longer fail due to too long reasons
|
||||
- Fixed some fields not preserving inline after string replacements
|
||||
|
||||
### Changed
|
||||
- `images.json` moved to `images.yml`
|
||||
- Links will use the new cdn url
|
||||
- Heads and Tails images will be updated if you haven't changed them already
|
||||
- `.slot` redesigned (and updated entries in `images.yml`)
|
||||
- Reduced required permissions for .qdel (thanks to tbodt)
|
||||
|
||||
## [3.0.7] - 05.10.2021
|
||||
|
||||
### Added
|
||||
- `.streamsclear` re-added. It will remove all followed streams on the server.
|
||||
- `.gifts` now have 3 new ✂️ Haircut 🧻 ToiletPaper and 🥀 WiltedRose which **reduce** waifu's value
|
||||
- They are called negative gifts
|
||||
- They show up at the end of the `.gifts` page and are marked with a broken heart
|
||||
- They have a separate multiplier (`waifu.multi.negative_gift_effect` default 0.5, changeable via `.config gambling` or `data/gambling.yml`)
|
||||
- When gifted, the waifu's price will be reduced by the `price * multiplier`
|
||||
- Negative gifts don't show up in `.waifuinfo` nor is the record of them kept in the database
|
||||
|
||||
### Fixed
|
||||
- Fixed `%users%` and `%shard.usercount%` placeholders not showing correct values
|
||||
|
||||
## [3.0.6] - 27.09.2021
|
||||
|
||||
### Added
|
||||
|
@@ -24,10 +24,11 @@ WORKDIR /app
|
||||
RUN set -xe; \
|
||||
useradd -m nadeko; \
|
||||
apt-get update; \
|
||||
apt-get install -y libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 sudo; \
|
||||
apt-get install -y libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 python3-pip sudo; \
|
||||
update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1; \
|
||||
echo 'Defaults>nadeko env_keep+="ASPNETCORE_* DOTNET_* NadekoBot_* shard_id total_shards TZ"' > /etc/sudoers.d/nadeko; \
|
||||
curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl; \
|
||||
pip3 install --upgrade youtube-dl; \
|
||||
apt-get remove -y python3-pip; \
|
||||
chmod +x /usr/local/bin/youtube-dl
|
||||
|
||||
COPY --from=build /app ./
|
||||
|
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Coordinator", "sr
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.VotesApi", "src\NadekoBot.VotesApi\NadekoBot.VotesApi.csproj", "{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -62,6 +64,12 @@ Global
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -73,6 +81,7 @@ Global
|
||||
{DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
{AE9B7F8C-81D7-4401-83A3-643B38258374} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}
|
||||
|
@@ -134,6 +134,8 @@ Compared to using tmux, this method requires a little bit more work to set up, b
|
||||
echo "[Unit]
|
||||
Description=NadekoBot service
|
||||
After=network.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=2
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -144,10 +146,11 @@ Compared to using tmux, this method requires a little bit more work to set up, b
|
||||
# source code.
|
||||
#ExecStartPre=/usr/bin/dotnet build ../src/NadekoBot/NadekoBot.csproj -c Release -o output/
|
||||
ExecStart=/usr/bin/dotnet NadekoBot.dll
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=NadekoBot
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" | sudo tee /etc/systemd/system/nadeko.service
|
||||
@@ -172,12 +175,16 @@ This method is similar to the one above, but requires one extra step, with the a
|
||||
echo "[Unit]
|
||||
Description=NadekoBot service
|
||||
After=network.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=2
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$USER
|
||||
WorkingDirectory=$PWD
|
||||
WorkingDirectory=$_WORKING_DIR
|
||||
ExecStart=/bin/bash NadekoRun.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=NadekoBot
|
||||
@@ -191,14 +198,14 @@ This method is similar to the one above, but requires one extra step, with the a
|
||||
4. Use the following command to create a script that will be used to start Nadeko:
|
||||
|
||||
```bash
|
||||
echo "#\!/bin/bash
|
||||
|
||||
echo \"\"
|
||||
echo \"Running NadekoBot in the background with auto restart\"
|
||||
{
|
||||
echo '#!/bin/bash'
|
||||
echo ""
|
||||
echo "echo \"Running NadekoBot in the background with auto restart\"
|
||||
youtube-dl -U
|
||||
|
||||
# If you want Nadeko to be compiled prior to every startup, uncomment the lines
|
||||
# below. Note that it's not neccessary unless you are personally modifying the
|
||||
# below. Note that it's not necessary unless you are personally modifying the
|
||||
# source code.
|
||||
#echo \"Compiling NadekoBot...\"
|
||||
#cd \"$PWD\"/nadekobot
|
||||
@@ -207,23 +214,33 @@ This method is similar to the one above, but requires one extra step, with the a
|
||||
echo \"Starting NadekoBot...\"
|
||||
|
||||
while true; do
|
||||
{
|
||||
cd \"$PWD\"/nadekobot/output
|
||||
dotnet NadekoBot.dll
|
||||
## If a non-zero exit code is produced, exit this script.
|
||||
} || {
|
||||
error_code=\"\$?\"
|
||||
if [[ -d $PWD/nadekobot/output ]]; then
|
||||
cd $PWD/nadekobot/output || {
|
||||
echo \"Failed to change working directory to $PWD/nadekobot/output\" >&2
|
||||
echo \"Ensure that the working directory inside of '/etc/systemd/system/nadeko.service' is correct\"
|
||||
echo \"Exiting...\"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo \"$PWD/nadekobot/output doesn't exist\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dotnet NadekoBot.dll || {
|
||||
echo \"An error occurred when trying to start NadekBot\"
|
||||
echo \"EXIT CODE: \$?\"
|
||||
exit \"\$error_code\"
|
||||
echo \"Exiting...\"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
echo \"Waiting for 5 seconds...\"
|
||||
sleep 5
|
||||
youtube-dl -U
|
||||
echo \"Restarting NadekoBot...\"
|
||||
done
|
||||
|
||||
echo \"Stopping NadekoBot...\"" > NadekoRun.sh
|
||||
echo \"Stopping NadekoBot...\""
|
||||
} > NadekoRun.sh
|
||||
```
|
||||
|
||||
5. Start Nadeko:
|
||||
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
|
||||
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
|
||||
|
@@ -2,13 +2,32 @@
|
||||
|
||||
Open Terminal (if you don't know how to, click on the magnifying glass on the top right corner of your screen and type **Terminal** on the window that pops up) and navigate to the location where you want to install the bot (for example `cd ~`)
|
||||
|
||||
##### Installing Homebrew and wget
|
||||
##### Installing Homebrew, wget and dotnet
|
||||
|
||||
###### Homebrew/wget
|
||||
*Skip this step if you already have homebrew installed*
|
||||
- Copy and paste this command, then press Enter:
|
||||
- `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
|
||||
- Install wget
|
||||
- `brew install wget`
|
||||
- `brew install wget`
|
||||
|
||||
###### Dotnet
|
||||
- Download [.net5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
|
||||
- Open the `.pkg` file you've downloaded and install it.
|
||||
- Run this command in Terminal. There might be output. If there is, disregard it. (copy-paste the entire block)
|
||||
```bash
|
||||
sudo mkdir /usr/local/bin
|
||||
|
||||
sudo mkdir /usr/local/lib
|
||||
```
|
||||
- Run this command in Terminal. There won't be any output. (copy-paste the entire block):
|
||||
```bash
|
||||
sudo ln -s /usr/local/share/dotnet/dotnet /usr/local/bin
|
||||
|
||||
sudo ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/
|
||||
|
||||
sudo ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/
|
||||
```
|
||||
|
||||
##### Installation Instructions
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
#### Prerequisites
|
||||
|
||||
- Windows 8 or later (64-bit)
|
||||
- [Create a Discord Bot application and invite the bot to your server](../../creds-guide.md)
|
||||
- [Create a Discord Bot application and invite the bot to your server](../creds-guide.md)
|
||||
|
||||
**Optional**
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
- Download and run the [NadekoBot v3 Updater][Updater].
|
||||
- Click on the + at the top left to create a new bot.
|
||||

|
||||

|
||||
- Give your bot a name and then click **`Go to setup`** at the lower right.
|
||||

|
||||

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

|
||||

|
||||
- Click on **`Install`** next to **`Redis`**.
|
||||
- **Note: If Redis fails to install, install Redis manually here: [Redis Installer](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.0.504) Download and run the **`.msi`** file.
|
||||
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DL`**.
|
||||
- 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.
|
||||
|
@@ -9,9 +9,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.38.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.41.0" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -171,7 +171,7 @@ namespace NadekoBot.Coordinator
|
||||
}
|
||||
}
|
||||
|
||||
status.Process?.Dispose();
|
||||
try { status.Process?.Dispose(); } catch { }
|
||||
|
||||
var proc = StartShardProcess(shardId);
|
||||
_shardStatuses[shardId] = status with
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
25
src/NadekoBot.VotesApi/.dockerignore
Normal file
25
src/NadekoBot.VotesApi/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
1
src/NadekoBot.VotesApi/.gitignore
vendored
Normal file
1
src/NadekoBot.VotesApi/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
store/
|
44
src/NadekoBot.VotesApi/Common/AuthHandler.cs
Normal file
44
src/NadekoBot.VotesApi/Common/AuthHandler.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NadekoBot.VotesApi.Controllers;
|
||||
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public class AuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string SchemeName = "AUTHORIZATION_SCHEME";
|
||||
public const string DiscordsClaim = "DISCORDS_CLAIM";
|
||||
public const string TopggClaim = "TOPGG_CLAIM";
|
||||
|
||||
private readonly IConfiguration _conf;
|
||||
|
||||
public AuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
IConfiguration conf)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
_conf = conf;
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new List<Claim>();
|
||||
|
||||
if (_conf[ConfKeys.DISCORDS_KEY].Trim() == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new(DiscordsClaim, "true"));
|
||||
|
||||
if (_conf[ConfKeys.TOPGG_KEY] == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new Claim(TopggClaim, "true"));
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(new(new(new ClaimsIdentity(claims)), SchemeName)));
|
||||
}
|
||||
}
|
||||
}
|
8
src/NadekoBot.VotesApi/Common/ConfKeys.cs
Normal file
8
src/NadekoBot.VotesApi/Common/ConfKeys.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public static class ConfKeys
|
||||
{
|
||||
public const string DISCORDS_KEY = "DiscordsKey";
|
||||
public const string TOPGG_KEY = "TopGGKey";
|
||||
}
|
||||
}
|
26
src/NadekoBot.VotesApi/Common/DiscordsVoteWebhookModel.cs
Normal file
26
src/NadekoBot.VotesApi/Common/DiscordsVoteWebhookModel.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public class DiscordsVoteWebhookModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user who voted
|
||||
/// </summary>
|
||||
public string User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the bot which recieved the vote
|
||||
/// </summary>
|
||||
public string Bot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains totalVotes, votesMonth, votes24, hasVoted - a list of IDs of users who have voted this month, and
|
||||
/// Voted24 - a list of IDs of users who have voted today
|
||||
/// </summary>
|
||||
public string Votes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of event, whether it is a vote event or test event
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
8
src/NadekoBot.VotesApi/Common/Policies.cs
Normal file
8
src/NadekoBot.VotesApi/Common/Policies.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public static class Policies
|
||||
{
|
||||
public const string DiscordsAuth = "DiscordsAuth";
|
||||
public const string TopggAuth = "TopggAuth";
|
||||
}
|
||||
}
|
30
src/NadekoBot.VotesApi/Common/TopggVoteWebhookModel.cs
Normal file
30
src/NadekoBot.VotesApi/Common/TopggVoteWebhookModel.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public class TopggVoteWebhookModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Discord ID of the bot that received a vote.
|
||||
/// </summary>
|
||||
public string Bot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Discord ID of the user who voted.
|
||||
/// </summary>
|
||||
public string User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the vote (should always be "upvote" except when using the test button it's "test").
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the weekend multiplier is in effect, meaning users votes count as two.
|
||||
/// </summary>
|
||||
public bool Weekend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2.
|
||||
/// </summary>
|
||||
public string Query { get; set; }
|
||||
}
|
||||
}
|
33
src/NadekoBot.VotesApi/Controllers/DiscordsController.cs
Normal file
33
src/NadekoBot.VotesApi/Controllers/DiscordsController.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NadekoBot.VotesApi.Services;
|
||||
|
||||
namespace NadekoBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class DiscordsController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TopGgController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public DiscordsController(ILogger<TopGgController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewDiscordsVotesAsync();
|
||||
if(votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes.", votes.Count);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
34
src/NadekoBot.VotesApi/Controllers/TopGgController.cs
Normal file
34
src/NadekoBot.VotesApi/Controllers/TopGgController.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NadekoBot.VotesApi.Services;
|
||||
|
||||
namespace NadekoBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class TopGgController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TopGgController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public TopGgController(ILogger<TopGgController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewTopGgVotesAsync();
|
||||
if(votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes.", votes.Count);
|
||||
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
52
src/NadekoBot.VotesApi/Controllers/WebhookController.cs
Normal file
52
src/NadekoBot.VotesApi/Controllers/WebhookController.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NadekoBot.VotesApi.Services;
|
||||
|
||||
namespace NadekoBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class WebhookController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<WebhookController> _logger;
|
||||
private readonly IVotesCache _votesCache;
|
||||
private readonly IConfiguration _conf;
|
||||
|
||||
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache, IConfiguration conf)
|
||||
{
|
||||
_logger = logger;
|
||||
_votesCache = votesCache;
|
||||
_conf = conf;
|
||||
}
|
||||
|
||||
[HttpPost("/discordswebhook")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IActionResult> DiscordsWebhook([FromBody]DiscordsVoteWebhookModel data)
|
||||
{
|
||||
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"discords.com");
|
||||
|
||||
await _votesCache.AddNewDiscordsVote(data.User);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("/topggwebhook")]
|
||||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IActionResult> TopggWebhook([FromBody] TopggVoteWebhookModel data)
|
||||
{
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"top.gg");
|
||||
|
||||
await _votesCache.AddNewTopggVote(data.User);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
20
src/NadekoBot.VotesApi/Dockerfile
Normal file
20
src/NadekoBot.VotesApi/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/NadekoBot.VotesApi/NadekoBot.VotesApi.csproj", "NadekoBot.VotesApi/"]
|
||||
RUN dotnet restore "src/NadekoBot.VotesApi/NadekoBot.VotesApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/NadekoBot.VotesApi"
|
||||
RUN dotnet build "NadekoBot.VotesApi.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "NadekoBot.VotesApi.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "NadekoBot.VotesApi.dll"]
|
13
src/NadekoBot.VotesApi/NadekoBot.VotesApi.csproj
Normal file
13
src/NadekoBot.VotesApi/NadekoBot.VotesApi.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
23
src/NadekoBot.VotesApi/Program.cs
Normal file
23
src/NadekoBot.VotesApi/Program.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
||||
}
|
||||
}
|
31
src/NadekoBot.VotesApi/Properties/launchSettings.json
Normal file
31
src/NadekoBot.VotesApi/Properties/launchSettings.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:16451",
|
||||
"sslPort": 44323
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"NadekoBot.VotesApi": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
src/NadekoBot.VotesApi/README.md
Normal file
46
src/NadekoBot.VotesApi/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Votes Api
|
||||
|
||||
This api is used if you want your bot to be able to reward users who vote for it on discords.com or top.gg
|
||||
|
||||
#### [GET] `/discords/new`
|
||||
Get the discords votes received after previous call to this endpoint.
|
||||
Input full url of this endpoint in your creds.yml file under Discords url field.
|
||||
For example "https://api.my.cool.bot/discords/new"
|
||||
#### [GET] `/topgg/new`
|
||||
Get the topgg votes received after previous call to this endpoint.
|
||||
Input full url of this endpoint in your creds.yml file under Topgg url field.
|
||||
For example "https://api.my.cool.bot/topgg/new"
|
||||
|
||||
#### [POST] `/discordswebhook`
|
||||
Input this endpoint as the webhook on discords.com bot edit page
|
||||
model: https://docs.botsfordiscord.com/methods/receiving-votes
|
||||
For example "https://api.my.cool.bot/topggwebhook"
|
||||
#### [POST] `/topggwebhook`
|
||||
Input this endpoint as the webhook https://top.gg/bot/:your-bot-id/webhooks (replace :your-bot-id with your bot's id)
|
||||
model: https://docs.top.gg/resources/webhooks/#schema
|
||||
For example "https://api.my.cool.bot/discordswebhook"
|
||||
|
||||
Input your super-secret header value in appsettings.json's DiscordsKey and TopGGKey fields
|
||||
They must match your DiscordsKey and TopGG key respectively, as well as your secrets in the discords.com and top.gg webhook setup pages
|
||||
|
||||
Full Example:
|
||||
|
||||
⚠ Change TopggKey and DiscordsKey to a secure long string
|
||||
⚠ You can use https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new to generate it
|
||||
|
||||
`creds.yml`
|
||||
```yml
|
||||
votes:
|
||||
TopggServiceUrl: "https://api.my.cool.bot/topgg"
|
||||
TopggKey: "my_topgg_key"
|
||||
DiscordsServiceUrl: "https://api.my.cool.bot/discords"
|
||||
DiscordsKey: "my_discords_key"
|
||||
```
|
||||
|
||||
`appsettings.json`
|
||||
```json
|
||||
...
|
||||
"DiscordsKey": "my_discords_key",
|
||||
"TopGGKey": "my_topgg_key",
|
||||
...
|
||||
```
|
105
src/NadekoBot.VotesApi/Services/FileVotesCache.cs
Normal file
105
src/NadekoBot.VotesApi/Services/FileVotesCache.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace NadekoBot.VotesApi.Services
|
||||
{
|
||||
public class FileVotesCache : IVotesCache
|
||||
{
|
||||
private const string statsFile = "store/stats.json";
|
||||
private const string topggFile = "store/topgg.json";
|
||||
private const string discordsFile = "store/discords.json";
|
||||
|
||||
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public FileVotesCache()
|
||||
{
|
||||
if (!Directory.Exists("store"))
|
||||
Directory.CreateDirectory("store");
|
||||
|
||||
if(!File.Exists(topggFile))
|
||||
File.WriteAllText(topggFile, "[]");
|
||||
|
||||
if(!File.Exists(discordsFile))
|
||||
File.WriteAllText(discordsFile, "[]");
|
||||
}
|
||||
|
||||
public ITask AddNewTopggVote(string userId)
|
||||
{
|
||||
return AddNewVote(topggFile, userId);
|
||||
}
|
||||
|
||||
public ITask AddNewDiscordsVote(string userId)
|
||||
{
|
||||
return AddNewVote(discordsFile, userId);
|
||||
}
|
||||
|
||||
private async ITask AddNewVote(string file, string userId)
|
||||
{
|
||||
await locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
var votes = await GetVotesAsync(file);
|
||||
votes.Add(userId);
|
||||
await File.WriteAllTextAsync(file , JsonSerializer.Serialize(votes));
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewTopGgVotesAsync()
|
||||
{
|
||||
var votes = await EvictTopggVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewDiscordsVotesAsync()
|
||||
{
|
||||
var votes = await EvictDiscordsVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
private ITask<List<Vote>> EvictTopggVotes()
|
||||
=> EvictVotes(topggFile);
|
||||
|
||||
private ITask<List<Vote>> EvictDiscordsVotes()
|
||||
=> EvictVotes(discordsFile);
|
||||
|
||||
private async ITask<List<Vote>> EvictVotes(string file)
|
||||
{
|
||||
await locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
|
||||
var ids = await GetVotesAsync(file);
|
||||
await File.WriteAllTextAsync(file, "[]");
|
||||
|
||||
return ids?
|
||||
.Select(x => (Ok: ulong.TryParse(x, out var r), Id: r))
|
||||
.Where(x => x.Ok)
|
||||
.Select(x => new Vote
|
||||
{
|
||||
UserId = x.Id
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ITask<IList<string>> GetVotesAsync(string file)
|
||||
{
|
||||
await using var fs = File.Open(file, FileMode.Open);
|
||||
var votes = await JsonSerializer.DeserializeAsync<List<string>>(fs);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
13
src/NadekoBot.VotesApi/Services/IVotesCache.cs
Normal file
13
src/NadekoBot.VotesApi/Services/IVotesCache.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace NadekoBot.VotesApi.Services
|
||||
{
|
||||
public interface IVotesCache
|
||||
{
|
||||
ITask<IList<Vote>> GetNewTopGgVotesAsync();
|
||||
ITask<IList<Vote>> GetNewDiscordsVotesAsync();
|
||||
ITask AddNewTopggVote(string userId);
|
||||
ITask AddNewDiscordsVote(string userId);
|
||||
}
|
||||
}
|
69
src/NadekoBot.VotesApi/Startup.cs
Normal file
69
src/NadekoBot.VotesApi/Startup.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NadekoBot.VotesApi.Services;
|
||||
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
services.AddSingleton<IVotesCache, FileVotesCache>();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "NadekoBot.VotesApi", Version = "v1" });
|
||||
});
|
||||
|
||||
services
|
||||
.AddAuthentication(opts =>
|
||||
{
|
||||
opts.DefaultScheme = AuthHandler.SchemeName;
|
||||
opts.AddScheme<AuthHandler>(AuthHandler.SchemeName, AuthHandler.SchemeName);
|
||||
});
|
||||
|
||||
services
|
||||
.AddAuthorization(opts =>
|
||||
{
|
||||
opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName)
|
||||
.RequireAssertion(x => false)
|
||||
.Build();
|
||||
opts.AddPolicy(Policies.DiscordsAuth, policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
|
||||
opts.AddPolicy(Policies.TopggAuth, policy => policy.RequireClaim(AuthHandler.TopggClaim));
|
||||
});
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NadekoBot.VotesApi v1"));
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
|
||||
}
|
||||
}
|
||||
}
|
9
src/NadekoBot.VotesApi/WeatherForecast.cs
Normal file
9
src/NadekoBot.VotesApi/WeatherForecast.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.VotesApi
|
||||
{
|
||||
public class Vote
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
}
|
||||
}
|
9
src/NadekoBot.VotesApi/appsettings.Development.json
Normal file
9
src/NadekoBot.VotesApi/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
12
src/NadekoBot.VotesApi/appsettings.json
Normal file
12
src/NadekoBot.VotesApi/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"DiscordsKey": "my_discords_key",
|
||||
"TopGGKey": "my_topgg_key",
|
||||
"AllowedHosts": "*"
|
||||
}
|
@@ -19,6 +19,7 @@ using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Common.Configs;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
using NadekoBot.Modules.Searches;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot
|
||||
@@ -28,7 +29,7 @@ namespace NadekoBot
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly CommandService _commandService;
|
||||
private readonly DbService _db;
|
||||
private readonly BotCredsProvider _credsProvider;
|
||||
private readonly IBotCredsProvider _credsProvider;
|
||||
|
||||
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
||||
|
||||
@@ -95,8 +96,8 @@ namespace NadekoBot
|
||||
}
|
||||
|
||||
var svcs = new ServiceCollection()
|
||||
.AddTransient<IBotCredentials>(_ => _creds) // bot creds
|
||||
.AddSingleton(_credsProvider)
|
||||
.AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
|
||||
.AddSingleton<IBotCredsProvider>(_credsProvider)
|
||||
.AddSingleton(_db) // database
|
||||
.AddRedis(_creds.RedisOptions) // redis
|
||||
.AddSingleton(Client) // discord socket client
|
||||
@@ -105,7 +106,7 @@ namespace NadekoBot
|
||||
.AddSingleton<ISeria, JsonSeria>()
|
||||
.AddSingleton<IPubSub, RedisPubSub>()
|
||||
.AddSingleton<IConfigSeria, YamlSeria>()
|
||||
.AddBotStringsServices()
|
||||
.AddBotStringsServices(_creds.TotalShards)
|
||||
.AddConfigServices()
|
||||
.AddConfigMigrators()
|
||||
.AddMemoryCache()
|
||||
@@ -145,7 +146,8 @@ namespace NadekoBot
|
||||
|
||||
svcs.Scan(scan => scan
|
||||
.FromAssemblyOf<IReadyExecutor>()
|
||||
.AddClasses(classes => classes.AssignableToAny(
|
||||
.AddClasses(classes => classes
|
||||
.AssignableToAny(
|
||||
// services
|
||||
typeof(INService),
|
||||
|
||||
|
@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Attributes
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
|
||||
{
|
||||
var creds = services.GetRequiredService<BotCredsProvider>().GetCreds();
|
||||
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
|
||||
|
||||
return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner")));
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ namespace NadekoBot.Common
|
||||
OwnerIds = new List<ulong>();
|
||||
TotalShards = 1;
|
||||
GoogleApiKey = string.Empty;
|
||||
Votes = new(string.Empty, string.Empty);
|
||||
Votes = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
BotListToken = string.Empty;
|
||||
CleverbotApiKey = string.Empty;
|
||||
@@ -73,16 +73,6 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
|
||||
Change only if you've changed the coordinator address or port.")]
|
||||
public string CoordinatorUrl { get; set; }
|
||||
|
||||
[YamlIgnore]
|
||||
public string PatreonCampaignId => Patreon?.CampaignId;
|
||||
[YamlIgnore]
|
||||
public string PatreonAccessToken => Patreon?.AccessToken;
|
||||
|
||||
[YamlIgnore]
|
||||
public string VotesUrl => Votes?.Url;
|
||||
[YamlIgnore]
|
||||
public string VotesToken => Votes.Key;
|
||||
|
||||
[Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
|
||||
public string RapidApiKey { get; set; }
|
||||
|
||||
@@ -126,11 +116,9 @@ Windows default
|
||||
// todo fixup patreon
|
||||
public sealed record PatreonSettings
|
||||
{
|
||||
[Comment(@"Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal")]
|
||||
public string ClientId { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
[Comment(@"Unused atm")]
|
||||
public string RefreshToken { get; set; }
|
||||
[Comment(@"Unused atm")]
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
[Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
|
||||
@@ -143,19 +131,44 @@ Windows default
|
||||
ClientSecret = clientSecret;
|
||||
CampaignId = campaignId;
|
||||
}
|
||||
|
||||
public PatreonSettings()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record VotesSettings
|
||||
{
|
||||
[Comment(@"")]
|
||||
public string Url { get; set; }
|
||||
[Comment(@"")]
|
||||
public string Key { get; set; }
|
||||
[Comment(@"top.gg votes service url
|
||||
This is the url of your instance of the NadekoBot.Votes api
|
||||
Example: https://votes.my.cool.bot.com")]
|
||||
public string TopggServiceUrl { get; set; }
|
||||
|
||||
[Comment(@"Authorization header value sent to the TopGG service url with each request
|
||||
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file")]
|
||||
public string TopggKey { get; set; }
|
||||
|
||||
[Comment(@"discords.com votes service url
|
||||
This is the url of your instance of the NadekoBot.Votes api
|
||||
Example: https://votes.my.cool.bot.com")]
|
||||
public string DiscordsServiceUrl { get; set; }
|
||||
|
||||
[Comment(@"Authorization header value sent to the Discords service url with each request
|
||||
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file")]
|
||||
public string DiscordsKey { get; set; }
|
||||
|
||||
public VotesSettings(string url, string key)
|
||||
public VotesSettings()
|
||||
{
|
||||
Url = url;
|
||||
Key = key;
|
||||
|
||||
}
|
||||
|
||||
public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey)
|
||||
{
|
||||
TopggServiceUrl = topggServiceUrl;
|
||||
TopggKey = topggKey;
|
||||
DiscordsServiceUrl = discordsServiceUrl;
|
||||
DiscordsKey = discordsKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,11 +14,15 @@ namespace NadekoBot.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddBotStringsServices(this IServiceCollection services)
|
||||
=> services
|
||||
.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
||||
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
|
||||
.AddSingleton<IBotStrings, BotStrings>();
|
||||
public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards)
|
||||
=> totalShards <= 1
|
||||
? services
|
||||
.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
||||
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
|
||||
.AddSingleton<IBotStrings, BotStrings>()
|
||||
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
||||
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
|
||||
.AddSingleton<IBotStrings, BotStrings>();
|
||||
|
||||
public static IServiceCollection AddConfigServices(this IServiceCollection services)
|
||||
{
|
||||
|
@@ -12,16 +12,14 @@ namespace NadekoBot
|
||||
string GoogleApiKey { get; }
|
||||
ICollection<ulong> OwnerIds { get; }
|
||||
string RapidApiKey { get; }
|
||||
string PatreonAccessToken { get; }
|
||||
|
||||
Creds.DbOptions Db { get; }
|
||||
string OsuApiKey { get; }
|
||||
int TotalShards { get; }
|
||||
string PatreonCampaignId { get; }
|
||||
Creds.PatreonSettings Patreon { get; }
|
||||
string CleverbotApiKey { get; }
|
||||
RestartConfig RestartCommand { get; }
|
||||
string VotesUrl { get; }
|
||||
string VotesToken { get; }
|
||||
Creds.VotesSettings Votes { get; }
|
||||
string BotListToken { get; }
|
||||
string RedisOptions { get; }
|
||||
string LocationIqApiKey { get; }
|
||||
|
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using NadekoBot.Common.Yml;
|
||||
|
||||
namespace NadekoBot.Common
|
||||
{
|
||||
public class ImageUrls
|
||||
{
|
||||
public int Version { get; set; } = 2;
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 3;
|
||||
|
||||
public CoinData Coins { get; set; }
|
||||
public Uri[] Currency { get; set; }
|
||||
@@ -25,7 +27,6 @@ namespace NadekoBot.Common
|
||||
public class SlotData
|
||||
{
|
||||
public Uri[] Emojis { get; set; }
|
||||
public Uri[] Numbers { get; set; }
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
|
||||
|
49
src/NadekoBot/Common/OldImageUrls.cs
Normal file
49
src/NadekoBot/Common/OldImageUrls.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Common
|
||||
{
|
||||
public class OldImageUrls
|
||||
{
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public CoinData Coins { get; set; }
|
||||
public Uri[] Currency { get; set; }
|
||||
public Uri[] Dice { get; set; }
|
||||
public RategirlData Rategirl { get; set; }
|
||||
public XpData Xp { get; set; }
|
||||
|
||||
//new
|
||||
public RipData Rip { get; set; }
|
||||
public SlotData Slots { get; set; }
|
||||
|
||||
public class RipData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
public Uri Overlay { get; set; }
|
||||
}
|
||||
|
||||
public class SlotData
|
||||
{
|
||||
public Uri[] Emojis { get; set; }
|
||||
public Uri[] Numbers { get; set; }
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
|
||||
public class CoinData
|
||||
{
|
||||
public Uri[] Heads { get; set; }
|
||||
public Uri[] Tails { get; set; }
|
||||
}
|
||||
|
||||
public class RategirlData
|
||||
{
|
||||
public Uri Matrix { get; set; }
|
||||
public Uri Dot { get; set; }
|
||||
}
|
||||
|
||||
public class XpData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -177,13 +177,13 @@ namespace NadekoBot.Common.Replacements
|
||||
/*OBSOLETE*/
|
||||
_reps.TryAdd("%servers%", () => c.Guilds.Count.ToString());
|
||||
#if !GLOBAL_NADEKO
|
||||
_reps.TryAdd("%users%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
|
||||
_reps.TryAdd("%users%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
#endif
|
||||
|
||||
/*NEW*/
|
||||
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
|
||||
#if !GLOBAL_NADEKO
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
#endif
|
||||
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
|
||||
return this;
|
||||
|
@@ -69,6 +69,7 @@ namespace NadekoBot.Common.Replacements
|
||||
var newF = new SmartTextEmbedField();
|
||||
newF.Name = Replace(f.Name);
|
||||
newF.Value = Replace(f.Value);
|
||||
newF.Inline = f.Inline;
|
||||
fields.Add(newF);
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Discord;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Services;
|
||||
@@ -29,6 +30,47 @@ namespace NadekoBot
|
||||
(Footer != null && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) ||
|
||||
(Fields != null && Fields.Length > 0);
|
||||
|
||||
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
|
||||
{
|
||||
var set = new SmartEmbedText();
|
||||
|
||||
set.PlainText = plainText;
|
||||
set.Title = eb.Title;
|
||||
set.Description = eb.Description;
|
||||
set.Url = eb.Url;
|
||||
set.Thumbnail = eb.Thumbnail?.Url;
|
||||
set.Image = eb.Image?.Url;
|
||||
set.Author = eb.Author is EmbedAuthor ea
|
||||
? new()
|
||||
{
|
||||
Name = ea.Name,
|
||||
Url = ea.Url,
|
||||
IconUrl = ea.IconUrl
|
||||
}
|
||||
: null;
|
||||
set.Footer = eb.Footer is EmbedFooter ef
|
||||
? new()
|
||||
{
|
||||
Text = ef.Text,
|
||||
IconUrl = ef.IconUrl
|
||||
}
|
||||
: null;
|
||||
|
||||
if (eb.Fields.Length > 0)
|
||||
set.Fields = eb
|
||||
.Fields
|
||||
.Select(field => new SmartTextEmbedField()
|
||||
{
|
||||
Inline = field.Inline,
|
||||
Name = field.Name,
|
||||
Value = field.Value,
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
set.Color = eb.Color?.RawValue ?? 0;
|
||||
return set;
|
||||
}
|
||||
|
||||
public EmbedBuilder GetEmbed()
|
||||
{
|
||||
var embed = new EmbedBuilder()
|
||||
|
17
src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs
Normal file
17
src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace NadekoBot.Common.TypeReaders
|
||||
{
|
||||
public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
|
||||
{
|
||||
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
if (!Emote.TryParse(input, out var emote))
|
||||
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid emote"));
|
||||
|
||||
return Task.FromResult(TypeReaderResult.FromSuccess(emote));
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@ namespace NadekoBot.Common.Yml
|
||||
.WithTypeConverter(new Rgba32Converter())
|
||||
.WithTypeConverter(new CultureInfoConverter())
|
||||
.WithTypeConverter(new UriConverter())
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using System;
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Discord;
|
||||
@@ -138,14 +139,15 @@ WHERE UserId={userId};");
|
||||
// just update the amount, there is no new user data
|
||||
if (!updatedUserData)
|
||||
{
|
||||
ctx.Database.ExecuteSqlInterpolated($@"
|
||||
var rows = ctx.Database.ExecuteSqlInterpolated($@"
|
||||
UPDATE OR IGNORE DiscordUser
|
||||
SET CurrencyAmount=CurrencyAmount+{amount}
|
||||
WHERE UserId={userId};
|
||||
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
|
||||
");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -157,8 +159,8 @@ SET CurrencyAmount=CurrencyAmount+{amount},
|
||||
AvatarId={avatarId}
|
||||
WHERE UserId={userId};
|
||||
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
|
||||
");
|
||||
}
|
||||
return true;
|
||||
@@ -167,7 +169,7 @@ VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
||||
public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
|
||||
{
|
||||
return users
|
||||
.Sum(x => x.CurrencyAmount);
|
||||
.Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
|
||||
}
|
||||
|
||||
public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
|
||||
|
@@ -54,7 +54,6 @@ namespace NadekoBot.Db
|
||||
.Include(gc => gc.CommandCooldowns)
|
||||
.Include(gc => gc.FollowedStreams)
|
||||
.Include(gc => gc.StreamRole)
|
||||
.Include(gc => gc.NsfwBlacklistedTags)
|
||||
.Include(gc => gc.XpSettings)
|
||||
.ThenInclude(x => x.ExclusionList)
|
||||
.Include(gc => gc.DelMsgOnCmdChannels)
|
||||
|
@@ -9,6 +9,11 @@ namespace NadekoBot.Db
|
||||
{
|
||||
public static class QuoteExtensions
|
||||
{
|
||||
public static IEnumerable<Quote> GetForGuild(this DbSet<Quote> quotes, ulong guildId)
|
||||
{
|
||||
return quotes.AsQueryable().Where(x => x.GuildId == guildId);
|
||||
}
|
||||
|
||||
public static IEnumerable<Quote> GetGroup(this DbSet<Quote> quotes, ulong guildId, int page, OrderType order)
|
||||
{
|
||||
var q = quotes.AsQueryable().Where(x => x.GuildId == guildId);
|
||||
|
@@ -18,5 +18,12 @@ namespace NadekoBot.Db
|
||||
.OrderBy(x => x.DateAdded)
|
||||
.Skip(page * 10)
|
||||
.Take(10);
|
||||
|
||||
public static IEnumerable<Reminder> RemindersForServer(this DbSet<Reminder> reminders, ulong serverId, int page)
|
||||
=> reminders.AsQueryable()
|
||||
.Where(x => x.ServerId == serverId)
|
||||
.OrderBy(x => x.DateAdded)
|
||||
.Skip(page * 10)
|
||||
.Take(10);
|
||||
}
|
||||
}
|
||||
|
12
src/NadekoBot/Db/Models/AutoTranslateChannel.cs
Normal file
12
src/NadekoBot/Db/Models/AutoTranslateChannel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Services.Database.Models
|
||||
{
|
||||
public class AutoTranslateChannel : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public bool AutoDelete { get; set; }
|
||||
public IList<AutoTranslateUser> Users { get; set; } = new List<AutoTranslateUser>();
|
||||
}
|
||||
}
|
11
src/NadekoBot/Db/Models/AutoTranslateUser.cs
Normal file
11
src/NadekoBot/Db/Models/AutoTranslateUser.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace NadekoBot.Services.Database.Models
|
||||
{
|
||||
public class AutoTranslateUser : DbEntity
|
||||
{
|
||||
public int ChannelId { get; set; }
|
||||
public AutoTranslateChannel Channel { get; set; }
|
||||
public ulong UserId { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string Target { get; set; }
|
||||
}
|
||||
}
|
@@ -91,7 +91,6 @@ namespace NadekoBot.Services.Database.Models
|
||||
public bool WarningsInitialized { get; set; }
|
||||
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
||||
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
|
||||
public HashSet<NsfwBlacklitedTag> NsfwBlacklistedTags { get; set; } = new HashSet<NsfwBlacklitedTag>();
|
||||
|
||||
public List<ShopEntry> ShopEntries { get; set; }
|
||||
public ulong? GameVoiceChannel { get; set; } = null;
|
||||
|
16
src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs
Normal file
16
src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Services.Database.Models
|
||||
{
|
||||
public class NsfwBlacklistedTag : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public string Tag { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Tag.GetHashCode(StringComparison.InvariantCulture);
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is NsfwBlacklistedTag x && x.Tag == Tag;
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Services.Database.Models
|
||||
{
|
||||
public class NsfwBlacklitedTag : DbEntity
|
||||
{
|
||||
public string Tag { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Tag.GetHashCode(StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is NsfwBlacklitedTag x
|
||||
? x.Tag == Tag
|
||||
: false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,5 +8,6 @@
|
||||
public bool Forgiven { get; set; }
|
||||
public string ForgivenBy { get; set; }
|
||||
public string Moderator { get; set; }
|
||||
public int Weight { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -59,6 +59,9 @@ namespace NadekoBot.Services.Database
|
||||
public DbSet<Poll> Poll { get; set; }
|
||||
public DbSet<WaifuInfo> WaifuInfo { get; set; }
|
||||
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
|
||||
public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
|
||||
public DbSet<AutoTranslateChannel> AutoTranslateChannels { get; set; }
|
||||
public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
|
||||
|
||||
public NadekoContext(DbContextOptions<NadekoContext> options) : base(options)
|
||||
{
|
||||
@@ -195,10 +198,16 @@ namespace NadekoBot.Services.Database
|
||||
#endregion
|
||||
|
||||
#region Warnings
|
||||
var warn = modelBuilder.Entity<Warning>();
|
||||
warn.HasIndex(x => x.GuildId);
|
||||
warn.HasIndex(x => x.UserId);
|
||||
warn.HasIndex(x => x.DateAdded);
|
||||
|
||||
modelBuilder.Entity<Warning>(warn =>
|
||||
{
|
||||
warn.HasIndex(x => x.GuildId);
|
||||
warn.HasIndex(x => x.UserId);
|
||||
warn.HasIndex(x => x.DateAdded);
|
||||
warn.Property(x => x.Weight)
|
||||
.HasDefaultValue(1);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region PatreonRewards
|
||||
@@ -357,6 +366,25 @@ namespace NadekoBot.Services.Database
|
||||
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc
|
||||
.HasIndex(x => x.ChannelId)
|
||||
.IsUnique());
|
||||
|
||||
modelBuilder.Entity<NsfwBlacklistedTag>(nbt => nbt
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique(false));
|
||||
|
||||
var atch = modelBuilder.Entity<AutoTranslateChannel>();
|
||||
atch.HasIndex(x => x.GuildId)
|
||||
.IsUnique(false);
|
||||
|
||||
atch.HasIndex(x => x.ChannelId)
|
||||
.IsUnique();
|
||||
|
||||
atch
|
||||
.HasMany(x => x.Users)
|
||||
.WithOne(x => x.Channel)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<AutoTranslateUser>(atu => atu
|
||||
.HasAlternateKey(x => new { x.ChannelId, x.UserId }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,12 @@ namespace NadekoBot.Migrations
|
||||
migrationBuilder.Sql("DELETE FROM FilterChannelId WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
|
||||
migrationBuilder.Sql("DELETE FROM CommandCooldown WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
|
||||
|
||||
// fix for users who edited their waifuinfo table manually and are unable to update
|
||||
migrationBuilder.Sql("DELETE FROM WaifuInfo where WaifuId not in (SELECT Id from DiscordUser);");
|
||||
|
||||
// fix for users who deleted clubs manually and are unable to update now
|
||||
migrationBuilder.Sql("UPDATE DiscordUser SET ClubId = null WHERE ClubId is not null and ClubId not in (SELECT Id from Clubs);");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChannelCreated",
|
||||
table: "LogSettings");
|
||||
|
2648
src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs
generated
Normal file
2648
src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace NadekoBot.Migrations
|
||||
{
|
||||
public partial class nsfwblacklisttags : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NsfwBlacklistedTags",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Tag = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NsfwBlacklistedTags", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NsfwBlacklistedTags_GuildId",
|
||||
table: "NsfwBlacklistedTags",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.Sql(@"INSERT INTO NsfwBlacklistedTags(Id, GuildId, Tag, DateAdded)
|
||||
SELECT
|
||||
Id,
|
||||
(SELECT GuildId From GuildConfigs WHERE Id=GuildConfigId),
|
||||
Tag,
|
||||
DateAdded
|
||||
FROM NsfwBlacklitedTag
|
||||
WHERE GuildConfigId in (SELECT Id from GuildConfigs);");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NsfwBlacklitedTag");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NsfwBlacklistedTags");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NsfwBlacklitedTag",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Tag = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NsfwBlacklitedTag", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NsfwBlacklitedTag_GuildConfigs_GuildConfigId",
|
||||
column: x => x.GuildConfigId,
|
||||
principalTable: "GuildConfigs",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NsfwBlacklitedTag_GuildConfigId",
|
||||
table: "NsfwBlacklitedTag",
|
||||
column: "GuildConfigId");
|
||||
}
|
||||
}
|
||||
}
|
2653
src/NadekoBot/Migrations/20211121002508_weighted-warnings.Designer.cs
generated
Normal file
2653
src/NadekoBot/Migrations/20211121002508_weighted-warnings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
src/NadekoBot/Migrations/20211121002508_weighted-warnings.cs
Normal file
24
src/NadekoBot/Migrations/20211121002508_weighted-warnings.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace NadekoBot.Migrations
|
||||
{
|
||||
public partial class weightedwarnings : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Weight",
|
||||
table: "Warnings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Weight",
|
||||
table: "Warnings");
|
||||
}
|
||||
}
|
||||
}
|
2725
src/NadekoBot/Migrations/20211213145407_atl-rework.Designer.cs
generated
Normal file
2725
src/NadekoBot/Migrations/20211213145407_atl-rework.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
71
src/NadekoBot/Migrations/20211213145407_atl-rework.cs
Normal file
71
src/NadekoBot/Migrations/20211213145407_atl-rework.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace NadekoBot.Migrations
|
||||
{
|
||||
public partial class atlrework : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AutoTranslateChannels",
|
||||
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),
|
||||
AutoDelete = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AutoTranslateChannels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AutoTranslateUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ChannelId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Source = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Target = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AutoTranslateUsers", x => x.Id);
|
||||
table.UniqueConstraint("AK_AutoTranslateUsers_ChannelId_UserId", x => new { x.ChannelId, x.UserId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AutoTranslateUsers_AutoTranslateChannels_ChannelId",
|
||||
column: x => x.ChannelId,
|
||||
principalTable: "AutoTranslateChannels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AutoTranslateChannels_ChannelId",
|
||||
table: "AutoTranslateChannels",
|
||||
column: "ChannelId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AutoTranslateChannels_GuildId",
|
||||
table: "AutoTranslateChannels",
|
||||
column: "GuildId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AutoTranslateUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AutoTranslateChannels");
|
||||
}
|
||||
}
|
||||
}
|
@@ -340,6 +340,62 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("AutoCommands");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AutoDelete")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("GuildId");
|
||||
|
||||
b.ToTable("AutoTranslateChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Target")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("ChannelId", "UserId");
|
||||
|
||||
b.ToTable("AutoTranslateUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1062,7 +1118,7 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("MutedUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -1071,7 +1127,7 @@ namespace NadekoBot.Migrations
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GuildConfigId")
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
@@ -1079,9 +1135,9 @@ namespace NadekoBot.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GuildConfigId");
|
||||
b.HasIndex("GuildId");
|
||||
|
||||
b.ToTable("NsfwBlacklitedTag");
|
||||
b.ToTable("NsfwBlacklistedTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
||||
@@ -1967,6 +2023,11 @@ namespace NadekoBot.Migrations
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DateAdded");
|
||||
@@ -2189,6 +2250,17 @@ namespace NadekoBot.Migrations
|
||||
b.Navigation("GuildConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b =>
|
||||
{
|
||||
b.HasOne("NadekoBot.Services.Database.Models.AutoTranslateChannel", "Channel")
|
||||
.WithMany("Users")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Channel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b =>
|
||||
{
|
||||
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
||||
@@ -2300,13 +2372,6 @@ namespace NadekoBot.Migrations
|
||||
.HasForeignKey("GuildConfigId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
|
||||
{
|
||||
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
||||
.WithMany("NsfwBlacklistedTags")
|
||||
.HasForeignKey("GuildConfigId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
||||
{
|
||||
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
||||
@@ -2543,6 +2608,11 @@ namespace NadekoBot.Migrations
|
||||
b.Navigation("IgnoredChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b =>
|
||||
{
|
||||
b.Navigation("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b =>
|
||||
{
|
||||
b.Navigation("AntiAltSetting");
|
||||
@@ -2573,8 +2643,6 @@ namespace NadekoBot.Migrations
|
||||
|
||||
b.Navigation("MutedUsers");
|
||||
|
||||
b.Navigation("NsfwBlacklistedTags");
|
||||
|
||||
b.Navigation("Permissions");
|
||||
|
||||
b.Navigation("ReactionRoleMessages");
|
||||
|
@@ -318,7 +318,7 @@ namespace NadekoBot.Modules.Administration
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[OwnerOnly]
|
||||
public async Task Die()
|
||||
public async Task Die(bool graceful = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -329,7 +329,7 @@ namespace NadekoBot.Modules.Administration
|
||||
// ignored
|
||||
}
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
_coord.Die();
|
||||
_coord.Die(graceful);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
@@ -506,6 +506,14 @@ namespace NadekoBot.Modules.Administration
|
||||
_strings.Reload();
|
||||
await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[OwnerOnly]
|
||||
public async Task CoordReload()
|
||||
{
|
||||
await _coord.Reload();
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus)
|
||||
{
|
||||
|
@@ -12,6 +12,8 @@ using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db;
|
||||
using Serilog;
|
||||
using System.Threading;
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Services
|
||||
{
|
||||
@@ -21,6 +23,11 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the (Message ID, User ID) of reaction roles that are currently being processed.
|
||||
/// </summary>
|
||||
private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new();
|
||||
|
||||
public RoleCommandsService(DiscordSocketClient client, DbService db,
|
||||
Bot bot)
|
||||
{
|
||||
@@ -38,75 +45,58 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
|
||||
private Task _client_ReactionAdded(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
if (!reaction.User.IsSpecified ||
|
||||
reaction.User.Value.IsBot ||
|
||||
reaction.User.Value is not SocketGuildUser gusr ||
|
||||
chan is not SocketGuildChannel gch ||
|
||||
!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||
return;
|
||||
|
||||
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
|
||||
|
||||
if (conf is null)
|
||||
return;
|
||||
|
||||
// compare emote names for backwards compatibility :facepalm:
|
||||
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
|
||||
|
||||
if (reactionRole != null)
|
||||
{
|
||||
if (!reaction.User.IsSpecified ||
|
||||
reaction.User.Value.IsBot ||
|
||||
!(reaction.User.Value is SocketGuildUser gusr))
|
||||
return;
|
||||
|
||||
if (!(chan is SocketGuildChannel gch))
|
||||
return;
|
||||
|
||||
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||
return;
|
||||
|
||||
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
|
||||
|
||||
if (conf is null)
|
||||
return;
|
||||
|
||||
// compare emote names for backwards compatibility :facepalm:
|
||||
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
|
||||
if (reactionRole != null)
|
||||
if (!conf.Exclusive)
|
||||
{
|
||||
if (conf.Exclusive)
|
||||
{
|
||||
var roleIds = conf.ReactionRoles.Select(x => x.RoleId)
|
||||
.Where(x => x != reactionRole.RoleId)
|
||||
.Select(x => gusr.Guild.GetRole(x))
|
||||
.Where(x => x != null);
|
||||
|
||||
var __ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
//if the role is exclusive,
|
||||
// remove all other reactions user added to the message
|
||||
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
foreach (var r in dl.Reactions)
|
||||
{
|
||||
if (r.Key.Name == reaction.Emote.Name)
|
||||
continue;
|
||||
try { await dl.RemoveReactionAsync(r.Key, gusr).ConfigureAwait(false); } catch { }
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
await gusr.RemoveRolesAsync(roleIds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var toAdd = gusr.Guild.GetRole(reactionRole.RoleId);
|
||||
if (toAdd != null && !gusr.Roles.Contains(toAdd))
|
||||
{
|
||||
await gusr.AddRolesAsync(new[] { toAdd }).ConfigureAwait(false);
|
||||
}
|
||||
await AddReactionRoleAsync(gusr, reactionRole);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
// If same (message, user) are being processed in an exclusive rero, quit
|
||||
if (!_reacting.Add((msg.Id, reaction.UserId)))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
|
||||
new RequestOptions()
|
||||
{
|
||||
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
|
||||
}).ConfigureAwait(false);
|
||||
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
|
||||
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg, gusr, reaction, conf, reactionRole, CancellationToken.None);
|
||||
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
|
||||
|
||||
await Task.WhenAll(removeExclusiveTask, addRoleTask).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Free (message/user) for another exclusive rero
|
||||
_reacting.TryRemove((msg.Id, reaction.UserId));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
else
|
||||
{
|
||||
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
|
||||
new RequestOptions()
|
||||
{
|
||||
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
|
||||
}).ConfigureAwait(false);
|
||||
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -114,16 +104,16 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
|
||||
private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!reaction.User.IsSpecified ||
|
||||
reaction.User.Value.IsBot ||
|
||||
!(reaction.User.Value is SocketGuildUser gusr))
|
||||
reaction.User.Value is not SocketGuildUser gusr)
|
||||
return;
|
||||
|
||||
if (!(chan is SocketGuildChannel gch))
|
||||
if (chan is not SocketGuildChannel gch)
|
||||
return;
|
||||
|
||||
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||
@@ -193,5 +183,71 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
uow.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reaction role to the specified user.
|
||||
/// </summary>
|
||||
/// <param name="user">A Discord guild user.</param>
|
||||
/// <param name="dbRero">The database settings of this reaction role.</param>
|
||||
private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero)
|
||||
{
|
||||
var toAdd = user.Guild.GetRole(dbRero.RoleId);
|
||||
|
||||
return (toAdd != null && !user.Roles.Contains(toAdd))
|
||||
? user.AddRoleAsync(toAdd)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the exclusive reaction roles and reactions from the specified user.
|
||||
/// </summary>
|
||||
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
|
||||
/// <param name="user">A Discord guild user.</param>
|
||||
/// <param name="reaction">The Discord reaction of the user.</param>
|
||||
/// <param name="dbReroMsg">The database entry of the reaction role message.</param>
|
||||
/// <param name="dbRero">The database settings of this reaction role.</param>
|
||||
/// <param name="cToken">A cancellation token to cancel the operation.</param>
|
||||
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
|
||||
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
|
||||
private Task RemoveExclusiveReactionRoleAsync(Cacheable<IUserMessage, ulong> reactionMessage, SocketGuildUser user, SocketReaction reaction, ReactionRoleMessage dbReroMsg, ReactionRole dbRero, CancellationToken cToken = default)
|
||||
{
|
||||
cToken.ThrowIfCancellationRequested();
|
||||
|
||||
var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId)
|
||||
.Where(x => x != dbRero.RoleId)
|
||||
.Select(x => user.Guild.GetRole(x))
|
||||
.Where(x => x != null);
|
||||
|
||||
var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken);
|
||||
|
||||
var removeRolesTask = user.RemoveRolesAsync(roleIds);
|
||||
|
||||
return Task.WhenAll(removeReactionsTask, removeRolesTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes old reactions from an exclusive reaction role.
|
||||
/// </summary>
|
||||
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
|
||||
/// <param name="user">A Discord guild user.</param>
|
||||
/// <param name="reaction">The Discord reaction of the user.</param>
|
||||
/// <param name="cToken">A cancellation token to cancel the operation.</param>
|
||||
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
|
||||
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
|
||||
private async Task RemoveOldReactionsAsync(Cacheable<IUserMessage, ulong> reactionMessage, SocketGuildUser user, SocketReaction reaction, CancellationToken cToken = default)
|
||||
{
|
||||
cToken.ThrowIfCancellationRequested();
|
||||
|
||||
//if the role is exclusive,
|
||||
// remove all other reactions user added to the message
|
||||
var dl = await reactionMessage.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
foreach (var r in dl.Reactions)
|
||||
{
|
||||
if (r.Key.Name == reaction.Emote.Name)
|
||||
continue;
|
||||
try { await dl.RemoveReactionAsync(r.Key, user).ConfigureAwait(false); } catch { }
|
||||
await Task.Delay(100, cToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -41,8 +41,11 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
}, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(12));
|
||||
}
|
||||
|
||||
public async Task<WarningPunishment> Warn(IGuild guild, ulong userId, IUser mod, string reason)
|
||||
public async Task<WarningPunishment> Warn(IGuild guild, ulong userId, IUser mod, int weight, string reason)
|
||||
{
|
||||
if (weight <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(weight));
|
||||
|
||||
var modName = mod.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
@@ -57,6 +60,7 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
Forgiven = false,
|
||||
Reason = reason,
|
||||
Moderator = modName,
|
||||
Weight = weight,
|
||||
};
|
||||
|
||||
int warnings = 1;
|
||||
@@ -70,7 +74,7 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
.Warnings
|
||||
.ForId(guildId, userId)
|
||||
.Where(w => !w.Forgiven && w.UserId == userId)
|
||||
.Count();
|
||||
.Sum(x => x.Weight);
|
||||
|
||||
uow.Warnings.Add(warn);
|
||||
|
||||
@@ -95,6 +99,10 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
public async Task ApplyPunishment(IGuild guild, IGuildUser user, IUser mod, PunishmentAction p, int minutes,
|
||||
ulong? roleId, string reason)
|
||||
{
|
||||
|
||||
if (!await CheckPermission(guild, p))
|
||||
return;
|
||||
|
||||
switch (p)
|
||||
{
|
||||
case PunishmentAction.Mute:
|
||||
@@ -167,6 +175,40 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent the bot from hitting 403's when it needs to
|
||||
/// apply punishments with insufficient permissions
|
||||
/// </summary>
|
||||
/// <param name="guild">Guild the punishment is applied in</param>
|
||||
/// <param name="punish">Punishment to apply</param>
|
||||
/// <returns>Whether the bot has sufficient permissions</returns>
|
||||
private async Task<bool> CheckPermission(IGuild guild, PunishmentAction punish)
|
||||
{
|
||||
|
||||
var botUser = await guild.GetCurrentUserAsync();
|
||||
switch (punish)
|
||||
{
|
||||
case PunishmentAction.Mute:
|
||||
return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles;
|
||||
case PunishmentAction.Kick:
|
||||
return botUser.GuildPermissions.KickMembers;
|
||||
case PunishmentAction.Ban:
|
||||
return botUser.GuildPermissions.BanMembers;
|
||||
case PunishmentAction.Softban:
|
||||
return botUser.GuildPermissions.BanMembers; // ban + unban
|
||||
case PunishmentAction.RemoveRoles:
|
||||
return botUser.GuildPermissions.ManageRoles;
|
||||
case PunishmentAction.ChatMute:
|
||||
return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role
|
||||
case PunishmentAction.VoiceMute:
|
||||
return botUser.GuildPermissions.MuteMembers;
|
||||
case PunishmentAction.AddRole:
|
||||
return botUser.GuildPermissions.ManageRoles;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CheckAllWarnExpiresAsync()
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
|
@@ -54,8 +54,17 @@ namespace NadekoBot.Modules.Administration
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
public async Task Warn(IGuildUser user, [Leftover] string reason = null)
|
||||
public Task Warn(IGuildUser user, [Leftover] string reason = null)
|
||||
=> Warn(1, user, reason);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null)
|
||||
{
|
||||
if (weight <= 0)
|
||||
return;
|
||||
|
||||
if (!await CheckRoleHierarchy(user))
|
||||
return;
|
||||
|
||||
@@ -76,7 +85,7 @@ namespace NadekoBot.Modules.Administration
|
||||
WarningPunishment punishment;
|
||||
try
|
||||
{
|
||||
punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, reason).ConfigureAwait(false);
|
||||
punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -230,19 +239,29 @@ namespace NadekoBot.Modules.Administration
|
||||
}
|
||||
else
|
||||
{
|
||||
var descText = GetText(strs.warn_count(
|
||||
Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()),
|
||||
Format.Bold(warnings.Sum(x => x.Weight).ToString())));
|
||||
|
||||
embed.WithDescription(descText);
|
||||
|
||||
var i = page * 9;
|
||||
foreach (var w in warnings)
|
||||
{
|
||||
i++;
|
||||
var name = GetText(strs.warned_on_by(
|
||||
w.DateAdded.Value.ToString("dd.MM.yyy"),
|
||||
w.DateAdded.Value.ToString("HH:mm"),
|
||||
w.DateAdded?.ToString("dd.MM.yyy"),
|
||||
w.DateAdded?.ToString("HH:mm"),
|
||||
w.Moderator));
|
||||
|
||||
if (w.Forgiven)
|
||||
name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
|
||||
|
||||
embed.AddField($"#`{i}` " + name, w.Reason.TrimTo(1020));
|
||||
|
||||
embed.AddField($"#`{i}` " + name,
|
||||
Format.Code(GetText(strs.warn_weight(w.Weight))) +
|
||||
'\n' +
|
||||
w.Reason.TrimTo(1000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +469,7 @@ namespace NadekoBot.Modules.Administration
|
||||
}
|
||||
}
|
||||
|
||||
await _mute.TimedBan(ctx.Guild, user, time.Time, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await _mute.TimedBan(ctx.Guild, user, time.Time, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
var toSend = _eb.Create().WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
@@ -476,7 +495,7 @@ namespace NadekoBot.Modules.Administration
|
||||
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||
if (user is null)
|
||||
{
|
||||
await ctx.Guild.AddBanAsync(userId, 7, ctx.User.ToString() + " | " + msg);
|
||||
await ctx.Guild.AddBanAsync(userId, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512));
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
@@ -516,7 +535,7 @@ namespace NadekoBot.Modules.Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await ctx.Guild.AddBanAsync(user, 7, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await ctx.Guild.AddBanAsync(user, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
|
||||
var toSend = _eb.Create().WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
@@ -692,7 +711,7 @@ namespace NadekoBot.Modules.Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await ctx.Guild.AddBanAsync(user, 7, "Softban | " + ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await ctx.Guild.AddBanAsync(user, 7, ("Softban | " + ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
|
||||
catch { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
|
||||
|
||||
@@ -749,7 +768,7 @@ namespace NadekoBot.Modules.Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await user.KickAsync(ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await user.KickAsync((ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
|
||||
var toSend = _eb.Create().WithOkColor()
|
||||
.WithTitle(GetText(strs.kicked_user))
|
||||
@@ -776,23 +795,32 @@ namespace NadekoBot.Modules.Administration
|
||||
return;
|
||||
|
||||
var missing = new List<string>();
|
||||
var banning = new HashSet<IGuildUser>();
|
||||
var banning = new HashSet<IUser>();
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
foreach (var userStr in userStrings)
|
||||
{
|
||||
if (ulong.TryParse(userStr, out var userId))
|
||||
{
|
||||
var user = await ctx.Guild.GetUserAsync(userId) ??
|
||||
IUser user = await ctx.Guild.GetUserAsync(userId) ??
|
||||
await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
missing.Add(userStr);
|
||||
continue;
|
||||
// if IGuildUser is null, try to get IUser
|
||||
user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId);
|
||||
|
||||
// only add to missing if *still* null
|
||||
if (user is null)
|
||||
{
|
||||
missing.Add(userStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!await CheckRoleHierarchy(user))
|
||||
//Hierachy checks only if the user is in the guild
|
||||
if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -820,7 +848,7 @@ namespace NadekoBot.Modules.Administration
|
||||
{
|
||||
try
|
||||
{
|
||||
await toBan.BanAsync(7);
|
||||
await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@@ -149,7 +149,7 @@ namespace NadekoBot.Modules.CustomReactions
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithDescription($"#{id}")
|
||||
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
|
||||
.AddField(GetText(strs.response), (found.Response + "\n```css\n" + found.Response).TrimTo(1020) + "```")
|
||||
.AddField(GetText(strs.response), found.Response.TrimTo(1000).Replace("](", "]\\("))
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,7 @@ namespace NadekoBot.Modules.CustomReactions
|
||||
_ = ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
var serialized = _service.ExportCrs(ctx.Guild?.Id);
|
||||
using var stream = await serialized.ToStream();
|
||||
await using var stream = await serialized.ToStream();
|
||||
await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null);
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using Cloneable;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Yml;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common
|
||||
{
|
||||
@@ -19,10 +21,11 @@ namespace NadekoBot.Modules.Gambling.Common
|
||||
Generation = new GenerationConfig();
|
||||
Timely = new TimelyConfig();
|
||||
Decay = new DecayConfig();
|
||||
Slots = new SlotsConfig();
|
||||
}
|
||||
|
||||
[Comment(@"DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 1;
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
[Comment(@"Currency settings")]
|
||||
public CurrencyConfig Currency { get; set; }
|
||||
@@ -59,6 +62,13 @@ Set 0 for unlimited")]
|
||||
[Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
|
||||
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
|
||||
public decimal PatreonCurrencyPerCent { get; set; } = 1;
|
||||
|
||||
[Comment(@"Currency reward per vote.
|
||||
This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting")]
|
||||
public long VoteReward { get; set; } = 100;
|
||||
|
||||
[Comment(@"Slot config")]
|
||||
public SlotsConfig Slots { get; set; }
|
||||
}
|
||||
|
||||
public class CurrencyConfig
|
||||
@@ -179,7 +189,8 @@ default is 0.02, which is 2%")]
|
||||
|
||||
public MultipliersData Multipliers { get; set; } = new MultipliersData();
|
||||
|
||||
[Comment(@"List of items available for gifting.")]
|
||||
[Comment(@"List of items available for gifting.
|
||||
If negative is true, gift will instead reduce waifu value.")]
|
||||
public List<WaifuItemModel> Items { get; set; } = new List<WaifuItemModel>();
|
||||
|
||||
public WaifuConfig()
|
||||
@@ -260,6 +271,17 @@ Default 1 (meaning no effect)")]
|
||||
Default 0.95 (meaning 95%)
|
||||
Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")]
|
||||
public decimal GiftEffect { get; set; } = 0.95M;
|
||||
|
||||
[Comment(@"What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'.
|
||||
Default 0.5 (meaning 50%)
|
||||
Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)")]
|
||||
public decimal NegativeGiftEffect { get; set; } = 0.50M;
|
||||
}
|
||||
|
||||
public sealed partial class SlotsConfig
|
||||
{
|
||||
[Comment(@"Hex value of the color which the numbers on the slot image will have.")]
|
||||
public Rgba32 CurrencyFontColor { get; set; } = SixLabors.ImageSharp.Color.Red;
|
||||
}
|
||||
|
||||
[Cloneable]
|
||||
@@ -268,19 +290,24 @@ Example: If a waifu is worth 1000, and she receives a gift worth 100, her new va
|
||||
public string ItemEmoji { get; set; }
|
||||
public int Price { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
|
||||
public bool Negative { get; set; }
|
||||
|
||||
public WaifuItemModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public WaifuItemModel(string itemEmoji, int price, string name)
|
||||
public WaifuItemModel(string itemEmoji, int price, string name, bool negative = false)
|
||||
{
|
||||
ItemEmoji = itemEmoji;
|
||||
Price = price;
|
||||
Name = name;
|
||||
Negative = negative;
|
||||
}
|
||||
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
|
8
src/NadekoBot/Modules/Gambling/Common/GamblingError.cs
Normal file
8
src/NadekoBot/Modules/Gambling/Common/GamblingError.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
public enum GamblingError
|
||||
{
|
||||
None,
|
||||
NotEnough
|
||||
}
|
||||
}
|
44
src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs
Normal file
44
src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.Slot
|
||||
{
|
||||
public class SlotGame
|
||||
{
|
||||
public class Result
|
||||
{
|
||||
public float Multiplier { get; }
|
||||
public int[] Rolls { get; }
|
||||
|
||||
public Result(float multiplier, int[] rolls)
|
||||
{
|
||||
Multiplier = multiplier;
|
||||
Rolls = rolls;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Random _rng = new NadekoRandom();
|
||||
|
||||
public SlotGame()
|
||||
{
|
||||
}
|
||||
|
||||
public Result Spin()
|
||||
{
|
||||
var rolls = new int[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) };
|
||||
var multi = 0;
|
||||
|
||||
if (rolls.All(x => x == 5))
|
||||
multi = 30;
|
||||
else if (rolls.All(x => x == rolls[0]))
|
||||
multi = 10;
|
||||
else if (rolls.Count(x => x == 5) == 2)
|
||||
multi = 4;
|
||||
else if (rolls.Any(x => x == 5))
|
||||
multi = 1;
|
||||
|
||||
return new Result(multi, rolls);
|
||||
}
|
||||
}
|
||||
}
|
12
src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs
Normal file
12
src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
public class SlotResponse
|
||||
{
|
||||
public float Multiplier { get; set; }
|
||||
public long Won { get; set; }
|
||||
public List<int> Rolls { get; set; } = new List<int>();
|
||||
public GamblingError Error { get; set; }
|
||||
}
|
||||
}
|
@@ -22,15 +22,12 @@ namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
private readonly IImageCache _images;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly DbService _db;
|
||||
private static readonly NadekoRandom rng = new NadekoRandom();
|
||||
|
||||
public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db,
|
||||
GamblingConfigService gss) : base(gss)
|
||||
public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss)
|
||||
{
|
||||
_images = data.LocalImages;
|
||||
_cs = cs;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
@@ -66,11 +66,11 @@ namespace NadekoBot.Modules.Gambling
|
||||
}
|
||||
var embed = _eb.Create()
|
||||
.WithTitle(GetText(strs.economy_state))
|
||||
.AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)) + CurrencySign)
|
||||
.AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)).ToString("N", _enUsCulture) + CurrencySign)
|
||||
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
|
||||
.AddField(GetText(strs.currency_planted), ((BigInteger)ec.Planted) + CurrencySign)
|
||||
.AddField(GetText(strs.owned_waifus_total), ((BigInteger)ec.Waifus) + CurrencySign)
|
||||
.AddField(GetText(strs.bot_currency), ec.Bot + CurrencySign)
|
||||
.AddField(GetText(strs.bot_currency), ec.Bot.ToString("N", _enUsCulture) + CurrencySign)
|
||||
.AddField(GetText(strs.total), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign)
|
||||
.WithOkColor();
|
||||
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
|
||||
@@ -247,25 +247,33 @@ namespace NadekoBot.Modules.Gambling
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
[Priority(0)]
|
||||
public Task Award(ShmartNumber amount, IGuildUser usr, [Leftover] string msg) =>
|
||||
public Task Award(long amount, IGuildUser usr, [Leftover] string msg) =>
|
||||
Award(amount, usr.Id, msg);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
[Priority(1)]
|
||||
public Task Award(ShmartNumber amount, [Leftover] IGuildUser usr) =>
|
||||
public Task Award(long amount, [Leftover] IGuildUser usr) =>
|
||||
Award(amount, usr.Id);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[OwnerOnly]
|
||||
[Priority(2)]
|
||||
public async Task Award(ShmartNumber amount, ulong usrId, [Leftover] string msg = null)
|
||||
public async Task Award(long amount, ulong usrId, [Leftover] string msg = null)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
await _cs.AddAsync(usrId,
|
||||
var usr = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(usrId);
|
||||
|
||||
if(usr is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await _cs.AddAsync(usr,
|
||||
$"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {(msg ?? "")}",
|
||||
amount,
|
||||
gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false);
|
||||
@@ -275,8 +283,8 @@ namespace NadekoBot.Modules.Gambling
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
[Priority(2)]
|
||||
public async Task Award(ShmartNumber amount, [Leftover] IRole role)
|
||||
[Priority(3)]
|
||||
public async Task Award(long amount, [Leftover] IRole role)
|
||||
{
|
||||
var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false))
|
||||
.Where(u => u.GetRoles().Contains(role))
|
||||
@@ -284,7 +292,7 @@ namespace NadekoBot.Modules.Gambling
|
||||
|
||||
await _cs.AddBulkAsync(users.Select(x => x.Id),
|
||||
users.Select(x => $"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
|
||||
users.Select(x => amount.Value),
|
||||
users.Select(x => amount),
|
||||
gamble: true)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -298,13 +306,13 @@ namespace NadekoBot.Modules.Gambling
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
[Priority(0)]
|
||||
public async Task Take(ShmartNumber amount, [Leftover] IRole role)
|
||||
public async Task Take(long amount, [Leftover] IRole role)
|
||||
{
|
||||
var users = (await role.GetMembersAsync()).ToList();
|
||||
|
||||
await _cs.RemoveBulkAsync(users.Select(x => x.Id),
|
||||
users.Select(x => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
|
||||
users.Select(x => amount.Value),
|
||||
users.Select(x => amount),
|
||||
gamble: true)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -318,7 +326,7 @@ namespace NadekoBot.Modules.Gambling
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
[Priority(1)]
|
||||
public async Task Take(ShmartNumber amount, [Leftover] IGuildUser user)
|
||||
public async Task Take(long amount, [Leftover] IGuildUser user)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
@@ -333,7 +341,7 @@ namespace NadekoBot.Modules.Gambling
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[OwnerOnly]
|
||||
public async Task Take(ShmartNumber amount, [Leftover] ulong usrId)
|
||||
public async Task Take(long amount, [Leftover] ulong usrId)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
@@ -8,10 +8,11 @@ using NadekoBot.Modules.Gambling.Services;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
public partial class Games
|
||||
public partial class Gambling
|
||||
{
|
||||
[Group]
|
||||
public class PlantPickCommands : GamblingSubmodule<PlantPickService>
|
||||
@@ -53,7 +54,7 @@ namespace NadekoBot.Modules.Games
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Plant(int amount = 1, string pass = null)
|
||||
public async Task Plant(ShmartNumber amount, string pass = null)
|
||||
{
|
||||
if (amount < 1)
|
||||
return;
|
||||
@@ -63,18 +64,17 @@ namespace NadekoBot.Modules.Games
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass);
|
||||
if (!success)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough( CurrencySign));
|
||||
return;
|
||||
}
|
||||
|
||||
if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
|
||||
{
|
||||
logService.AddDeleteIgnore(ctx.Message.Id);
|
||||
await ctx.Message.DeleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass);
|
||||
if (!success)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough( CurrencySign));
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
@@ -8,8 +8,6 @@ using System.Threading.Tasks;
|
||||
using System;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using Serilog;
|
||||
|
||||
@@ -17,76 +15,22 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
public class CurrencyEventsService : INService
|
||||
{
|
||||
public class VoteModel
|
||||
{
|
||||
public ulong User { get; set; }
|
||||
public long Date { get; set; }
|
||||
}
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly GamblingConfigService _configService;
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
|
||||
new ConcurrentDictionary<ulong, ICurrencyEvent>();
|
||||
|
||||
public CurrencyEventsService(DiscordSocketClient client,
|
||||
IBotCredentials creds, ICurrencyService cs,
|
||||
IHttpClientFactory http, GamblingConfigService configService)
|
||||
|
||||
public CurrencyEventsService(
|
||||
DiscordSocketClient client,
|
||||
ICurrencyService cs,
|
||||
GamblingConfigService configService)
|
||||
{
|
||||
_client = client;
|
||||
_cs = cs;
|
||||
_creds = creds;
|
||||
_http = http;
|
||||
_configService = configService;
|
||||
|
||||
if (_client.ShardId == 0)
|
||||
{
|
||||
Task t = BotlistUpvoteLoop();
|
||||
}
|
||||
}
|
||||
|
||||
// todo future use votes api directly?
|
||||
private async Task BotlistUpvoteLoop()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.VotesUrl))
|
||||
return;
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
|
||||
await TriggerVoteCheck().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TriggerVoteCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, _creds.VotesUrl))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_creds.VotesToken))
|
||||
req.Headers.Add("Authorization", _creds.VotesToken);
|
||||
using (var http = _http.CreateClient())
|
||||
using (var res = await http.SendAsync(req).ConfigureAwait(false))
|
||||
{
|
||||
if (!res.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Warning("Botlist API not reached.");
|
||||
return;
|
||||
}
|
||||
var resStr = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var ids = JsonConvert.DeserializeObject<VoteModel[]>(resStr)
|
||||
.Select(x => x.User)
|
||||
.Distinct();
|
||||
await _cs.AddBulkAsync(ids, ids.Select(x => "Voted - <https://discordbots.org/bot/nadeko/vote>"), ids.Select(x => 10L), true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error in TriggerVoteCheck");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
|
||||
@@ -127,6 +71,7 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
@@ -136,4 +81,4 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,11 @@
|
||||
using NadekoBot.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Configs;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Services;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
@@ -34,9 +38,48 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
|
||||
AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
|
||||
AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
AddParsedProp("waifu.multi.negative_gift_effect", gs => gs.Waifu.Multipliers.NegativeGiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
|
||||
AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
|
||||
private readonly IEnumerable<WaifuItemModel> antiGiftSeed = new[]
|
||||
{
|
||||
new WaifuItemModel("🥀", 100, "WiltedRose", true),
|
||||
new WaifuItemModel("✂️", 1000, "Haircut", true),
|
||||
new WaifuItemModel("🧻", 10000, "ToiletPaper", true),
|
||||
};
|
||||
|
||||
public void Migrate()
|
||||
{
|
||||
if (_data.Version < 2)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Waifu.Items = c.Waifu.Items.Concat(antiGiftSeed).ToList();
|
||||
c.Version = 2;
|
||||
});
|
||||
}
|
||||
|
||||
if (_data.Version < 3)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 3;
|
||||
c.VoteReward = 100;
|
||||
});
|
||||
}
|
||||
|
||||
if (_data.Version < 4)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 4;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,12 +6,14 @@ using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Modules.Gambling.Common.Slot;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using Serilog;
|
||||
|
||||
@@ -82,6 +84,41 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
|
||||
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SlotResponse> SlotAsync(ulong userId, long amount)
|
||||
{
|
||||
var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true);
|
||||
|
||||
if (!takeRes)
|
||||
{
|
||||
return new SlotResponse
|
||||
{
|
||||
Error = GamblingError.NotEnough
|
||||
};
|
||||
}
|
||||
|
||||
var game = new SlotGame();
|
||||
var result = game.Spin();
|
||||
long won = 0;
|
||||
|
||||
if (result.Multiplier > 0)
|
||||
{
|
||||
won = (long)(result.Multiplier * amount);
|
||||
|
||||
await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true);
|
||||
}
|
||||
|
||||
var toReturn = new SlotResponse
|
||||
{
|
||||
Multiplier = result.Multiplier,
|
||||
Won = won,
|
||||
};
|
||||
|
||||
toReturn.Rolls.AddRange(result.Rolls);
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
|
||||
public struct EconomyResult
|
||||
{
|
||||
|
122
src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs
Normal file
122
src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Services;
|
||||
using Discord.WebSocket;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
public class VoteModel
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public ulong UserId { get; set; }
|
||||
}
|
||||
|
||||
public class VoteRewardService : INService, IReadyExecutor
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ICurrencyService _currencyService;
|
||||
private readonly GamblingConfigService _gamb;
|
||||
private HttpClient _http;
|
||||
|
||||
public VoteRewardService(
|
||||
DiscordSocketClient client,
|
||||
IBotCredentials creds,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ICurrencyService currencyService,
|
||||
GamblingConfigService gamb)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_currencyService = currencyService;
|
||||
_gamb = gamb;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
if (_client.ShardId != 0)
|
||||
return;
|
||||
|
||||
_http = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
ServerCertificateCustomValidationCallback = delegate { return true; }
|
||||
});
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(30000);
|
||||
|
||||
var topggKey = _creds.Votes?.TopggKey;
|
||||
var topggServiceUrl = _creds.Votes?.TopggServiceUrl;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(topggKey)
|
||||
&& !string.IsNullOrWhiteSpace(topggServiceUrl))
|
||||
{
|
||||
_http.DefaultRequestHeaders.Authorization = new(topggKey);
|
||||
var uri = new Uri(new(topggServiceUrl), "topgg/new");
|
||||
var res = await _http.GetStringAsync(uri);
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
data.Select(_ => "top.gg vote reward"),
|
||||
data.Select(x => _gamb.Data.VoteReward),
|
||||
true);
|
||||
|
||||
Log.Information("Rewarding {Count} top.gg voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading top.gg vote rewards.");
|
||||
}
|
||||
|
||||
var discordsKey = _creds.Votes?.DiscordsKey;
|
||||
var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(discordsKey)
|
||||
&& !string.IsNullOrWhiteSpace(discordsServiceUrl))
|
||||
{
|
||||
_http.DefaultRequestHeaders.Authorization = new(discordsKey);
|
||||
var res = await _http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new"));
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
data.Select(_ => "discords.com vote reward"),
|
||||
data.Select(x => _gamb.Data.VoteReward),
|
||||
true);
|
||||
|
||||
Log.Information("Rewarding {Count} discords.com voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading discords.com vote rewards.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -394,19 +394,28 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
});
|
||||
}
|
||||
|
||||
w.Items.Add(new WaifuItem()
|
||||
if (!itemObj.Negative)
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji,
|
||||
});
|
||||
|
||||
if (w.Claimer?.UserId == from.Id)
|
||||
{
|
||||
w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
w.Items.Add(new WaifuItem()
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji,
|
||||
});
|
||||
|
||||
if (w.Claimer?.UserId == from.Id)
|
||||
{
|
||||
w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Price += itemObj.Price / 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Price += itemObj.Price / 2;
|
||||
w.Price -= (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||
if (w.Price < 1)
|
||||
w.Price = 1;
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -512,7 +521,7 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
var conf = _gss.Data;
|
||||
return conf.Waifu.Items
|
||||
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name))
|
||||
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name, x.Negative))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
@@ -9,12 +9,17 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Services;
|
||||
using SixLabors.Fonts;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
@@ -33,12 +38,16 @@ namespace NadekoBot.Modules.Gambling
|
||||
//thanks to judge for helping me with this
|
||||
|
||||
private readonly IImageCache _images;
|
||||
private readonly ICurrencyService _cs;
|
||||
private FontProvider _fonts;
|
||||
private readonly DbService _db;
|
||||
|
||||
public SlotCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gamb) : base(gamb)
|
||||
public SlotCommands(IDataCache data,
|
||||
FontProvider fonts, DbService db,
|
||||
GamblingConfigService gamb) : base(gamb)
|
||||
{
|
||||
_images = data.LocalImages;
|
||||
_cs = cs;
|
||||
_fonts = fonts;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public sealed class SlotMachine
|
||||
@@ -140,92 +149,119 @@ namespace NadekoBot.Modules.Gambling
|
||||
[NadekoCommand, Aliases]
|
||||
public async Task Slot(ShmartNumber amount)
|
||||
{
|
||||
if (!_runningUsers.Add(ctx.User.Id))
|
||||
if (!_runningUsers.Add(ctx.User.Id))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
|
||||
return;
|
||||
const int maxAmount = 9999;
|
||||
if (amount > maxAmount)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.max_bet_limit(maxAmount + CurrencySign));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
|
||||
return;
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
|
||||
if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
|
||||
return;
|
||||
}
|
||||
Interlocked.Add(ref _totalBet, amount.Value);
|
||||
using (var bgImage = Image.Load(_images.SlotBackground))
|
||||
{
|
||||
var result = SlotMachine.Pull();
|
||||
int[] numbers = result.Numbers;
|
||||
var result = await _service.SlotAsync(ctx.User.Id, amount);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions()));
|
||||
}
|
||||
}
|
||||
if (result.Error != GamblingError.None)
|
||||
{
|
||||
if (result.Error == GamblingError.NotEnough)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
|
||||
}
|
||||
|
||||
var won = amount * result.Multiplier;
|
||||
var printWon = won;
|
||||
var n = 0;
|
||||
do
|
||||
{
|
||||
var digit = (int)(printWon % 10);
|
||||
using (var img = Image.Load(_images.SlotNumbers[digit]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(img, new Point(230 - n * 16, 462), new GraphicsOptions()));
|
||||
}
|
||||
n++;
|
||||
} while ((printWon /= 10) != 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var printAmount = amount;
|
||||
n = 0;
|
||||
do
|
||||
{
|
||||
var digit = (int)(printAmount % 10);
|
||||
using (var img = Image.Load(_images.SlotNumbers[digit]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(img, new Point(395 - n * 16, 462), new GraphicsOptions()));
|
||||
}
|
||||
n++;
|
||||
} while ((printAmount /= 10) != 0);
|
||||
Interlocked.Add(ref _totalBet, amount);
|
||||
Interlocked.Add(ref _totalPaidOut, result.Won);
|
||||
|
||||
var msg = GetText(strs.better_luck);
|
||||
if (result.Multiplier != 0)
|
||||
{
|
||||
await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false);
|
||||
Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
|
||||
if (result.Multiplier == 1)
|
||||
msg = GetText(strs.slot_single(CurrencySign, 1));
|
||||
else if (result.Multiplier == 4)
|
||||
msg = GetText(strs.slot_two(CurrencySign, 4));
|
||||
else if (result.Multiplier == 10)
|
||||
msg = GetText(strs.slot_three(10));
|
||||
else if (result.Multiplier == 30)
|
||||
msg = GetText(strs.slot_jackpot(30));
|
||||
}
|
||||
long ownedAmount;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
ownedAmount = uow.Set<DiscordUser>()
|
||||
.FirstOrDefault(x => x.UserId == ctx.User.Id)
|
||||
?.CurrencyAmount ?? 0;
|
||||
}
|
||||
|
||||
using (var imgStream = bgImage.ToStream())
|
||||
{
|
||||
await ctx.Channel.SendFileAsync(imgStream, "result.png", ctx.User.Mention + " " + msg + $"\n`{GetText(strs.slot_bet)}:`{amount} `{GetText(strs.won)}:` {amount * result.Multiplier}{CurrencySign}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_runningUsers.Remove(ctx.User.Id);
|
||||
});
|
||||
}
|
||||
using (var bgImage = Image.Load<Rgba32>(_images.SlotBackground, out var format))
|
||||
{
|
||||
var numbers = new int[3];
|
||||
result.Rolls.CopyTo(numbers, 0);
|
||||
|
||||
Color fontColor = _config.Slots.CurrencyFontColor;
|
||||
|
||||
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||
{
|
||||
TextOptions = new TextOptions()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
WrapTextWidth = 140,
|
||||
}
|
||||
}, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), fontColor,
|
||||
new PointF(227, 92)));
|
||||
|
||||
var bottomFont = _fonts.DottyFont.CreateFont(50);
|
||||
|
||||
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||
{
|
||||
TextOptions = new TextOptions()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
WrapTextWidth = 135,
|
||||
}
|
||||
}, amount.ToString(), bottomFont, fontColor,
|
||||
new PointF(129, 472)));
|
||||
|
||||
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||
{
|
||||
TextOptions = new TextOptions()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
WrapTextWidth = 135,
|
||||
}
|
||||
}, ownedAmount.ToString(), bottomFont, fontColor,
|
||||
new PointF(325, 472)));
|
||||
//sw.PrintLap("drew red text");
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
using (var img = Image.Load(_images.SlotEmojis[numbers[i]]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + 105 * i, 217), 1f));
|
||||
}
|
||||
}
|
||||
|
||||
var msg = GetText(strs.better_luck);
|
||||
if (result.Multiplier > 0)
|
||||
{
|
||||
if (result.Multiplier == 1f)
|
||||
msg = GetText(strs.slot_single(CurrencySign, 1));
|
||||
else if (result.Multiplier == 4f)
|
||||
msg = GetText(strs.slot_two(CurrencySign, 4));
|
||||
else if (result.Multiplier == 10f)
|
||||
msg = GetText(strs.slot_three(10));
|
||||
else if (result.Multiplier == 30f)
|
||||
msg = GetText(strs.slot_jackpot(30));
|
||||
}
|
||||
|
||||
using (var imgStream = bgImage.ToStream())
|
||||
{
|
||||
await ctx.Channel.SendFileAsync(imgStream,
|
||||
filename: "result.png",
|
||||
text: Format.Bold(ctx.User.ToString()) + " " + msg).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_runningUsers.Remove(ctx.User.Id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -317,10 +317,14 @@ namespace NadekoBot.Modules.Gambling
|
||||
.WithOkColor();
|
||||
|
||||
waifuItems
|
||||
.OrderBy(x => x.Price)
|
||||
.OrderBy(x => x.Negative)
|
||||
.ThenBy(x => x.Price)
|
||||
.Skip(9 * cur)
|
||||
.Take(9)
|
||||
.ForEach(x => embed.AddField($"{x.ItemEmoji} {x.Name}", x.Price, true));
|
||||
.ForEach(x => embed
|
||||
.AddField($"{(!x.Negative ? string.Empty : "\\💔")} {x.ItemEmoji} {x.Name}",
|
||||
Format.Bold(x.Price.ToString()) + _config.Currency.Sign,
|
||||
true));
|
||||
|
||||
return embed;
|
||||
}, waifuItems.Count, 9);
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using Discord.Commands;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Services;
|
||||
using NadekoBot.Db;
|
||||
@@ -23,6 +24,7 @@ namespace NadekoBot.Modules.Games
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NoPublicBot]
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
|
@@ -73,7 +73,6 @@ namespace NadekoBot.Modules.Games.Hangman
|
||||
return new(output);
|
||||
}
|
||||
|
||||
// todo lock
|
||||
public State Guess(string guess)
|
||||
{
|
||||
if (CurrentPhase != Phase.Running)
|
||||
|
@@ -763,7 +763,7 @@ namespace NadekoBot.Modules.Music
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[RequireUserPermission(GuildPermission.Administrator)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task MusicQuality()
|
||||
{
|
||||
var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id);
|
||||
@@ -772,7 +772,7 @@ namespace NadekoBot.Modules.Music
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[RequireUserPermission(GuildPermission.Administrator)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task MusicQuality(QualityPreset preset)
|
||||
{
|
||||
await _service.SetMusicQualityAsync(ctx.Guild.Id, preset);
|
||||
|
15
src/NadekoBot/Modules/Nsfw/Common/Booru.cs
Normal file
15
src/NadekoBot/Modules/Nsfw/Common/Booru.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public enum Booru
|
||||
{
|
||||
Safebooru,
|
||||
E621,
|
||||
Derpibooru,
|
||||
Rule34,
|
||||
Gelbooru,
|
||||
Konachan,
|
||||
Yandere,
|
||||
Danbooru,
|
||||
Sankaku
|
||||
}
|
||||
}
|
18
src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs
Normal file
18
src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class DapiImageObject : IImageData
|
||||
{
|
||||
[JsonPropertyName("File_Url")]
|
||||
public string FileUrl { get; set; }
|
||||
public string Tags { get; set; }
|
||||
[JsonPropertyName("Tag_String")]
|
||||
public string TagString { get; set; }
|
||||
public int Score { get; set; }
|
||||
public string Rating { get; set; }
|
||||
|
||||
public ImageData ToCachedImageData(Booru type)
|
||||
=> new ImageData(this.FileUrl, type, this.Tags?.Split(' ') ?? this.TagString?.Split(' '), Score.ToString() ?? Rating);
|
||||
}
|
||||
}
|
15
src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs
Normal file
15
src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public readonly struct DapiTag
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public DapiTag(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
19
src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs
Normal file
19
src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class DerpiContainer
|
||||
{
|
||||
public DerpiImageObject[] Images { get; set; }
|
||||
}
|
||||
|
||||
public class DerpiImageObject : IImageData
|
||||
{
|
||||
[JsonPropertyName("view_url")]
|
||||
public string ViewUrl { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
public int Score { get; set; }
|
||||
public ImageData ToCachedImageData(Booru type)
|
||||
=> new(ViewUrl, type, Tags, Score.ToString("F1"));
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public sealed class DanbooruImageDownloader : DapiImageDownloader
|
||||
{
|
||||
// using them as concurrent hashsets, value doesn't matter
|
||||
private static readonly ConcurrentDictionary<string, bool> _existentTags = new();
|
||||
private static readonly ConcurrentDictionary<string, bool> _nonexistentTags = new();
|
||||
|
||||
public override async Task<bool> IsTagValid(string tag, CancellationToken cancel = default)
|
||||
{
|
||||
if (_existentTags.ContainsKey(tag))
|
||||
return true;
|
||||
|
||||
if (_nonexistentTags.ContainsKey(tag))
|
||||
return false;
|
||||
|
||||
var tags = await _http.GetFromJsonAsync<DapiTag[]>(_baseUrl +
|
||||
"/tags.json" +
|
||||
$"?search[name_or_alias_matches]={tag}",
|
||||
options: this._serializerOptions,
|
||||
cancellationToken: cancel);
|
||||
if (tags is {Length: > 0})
|
||||
{
|
||||
return _existentTags[tag] = true;
|
||||
}
|
||||
|
||||
return _nonexistentTags[tag] = false;
|
||||
}
|
||||
|
||||
public DanbooruImageDownloader(HttpClient http)
|
||||
: base(Booru.Danbooru, http, "http://danbooru.donmai.us")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public abstract class DapiImageDownloader : ImageDownloader<DapiImageObject>
|
||||
{
|
||||
protected readonly string _baseUrl;
|
||||
|
||||
public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl) : base(booru, http)
|
||||
{
|
||||
_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public abstract Task<bool> IsTagValid(string tag, CancellationToken cancel = default);
|
||||
protected async Task<bool> AllTagsValid(string[] tags, CancellationToken cancel = default)
|
||||
{
|
||||
var results = await Task.WhenAll(tags.Select(tag => IsTagValid(tag, cancel)));
|
||||
|
||||
// if any of the tags is not valid, the query is not valid
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (!result)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override async Task<List<DapiImageObject>> DownloadImagesAsync(string[] tags, int page,
|
||||
bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
// up to 2 tags allowed on danbooru
|
||||
if (tags.Length > 2)
|
||||
return new();
|
||||
|
||||
if (!await AllTagsValid(tags, cancel).ConfigureAwait(false))
|
||||
return new();
|
||||
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
|
||||
|
||||
var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}";
|
||||
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
|
||||
.ConfigureAwait(false);
|
||||
if (imageObjects is null)
|
||||
return new();
|
||||
return imageObjects
|
||||
.Where(x => x.FileUrl is not null)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class DerpibooruImageDownloader : ImageDownloader<DerpiImageObject>
|
||||
{
|
||||
public DerpibooruImageDownloader(HttpClient http) : base(Booru.Derpibooru, http)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<List<DerpiImageObject>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
|
||||
var uri = $"https://www.derpibooru.org/api/v1/json/search/images?q={tagString.Replace('+', ',')}&per_page=49&page={page}";
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
req.Headers.AddFakeHeaders();
|
||||
using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
var container = await res.Content.ReadFromJsonAsync<DerpiContainer>(_serializerOptions, cancel).ConfigureAwait(false);
|
||||
if (container?.Images is null)
|
||||
return new();
|
||||
|
||||
return container.Images
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class E621ImageDownloader : ImageDownloader<E621Object>
|
||||
{
|
||||
public E621ImageDownloader(HttpClient http) : base(Booru.E621, http)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<List<E621Object>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: isExplicit);
|
||||
var uri = $"https://e621.net/posts.json?limit=32&tags={tagString}&page={page}";
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
req.Headers.AddFakeHeaders();
|
||||
using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
var data = await res.Content.ReadFromJsonAsync<E621Response>(_serializerOptions, cancel).ConfigureAwait(false);
|
||||
if (data?.Posts is null)
|
||||
return new();
|
||||
|
||||
return data.Posts
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class E621Response
|
||||
{
|
||||
public List<E621Object> Posts { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class GelbooruImageDownloader : ImageDownloader<DapiImageObject>
|
||||
{
|
||||
public GelbooruImageDownloader(HttpClient http) : base(Booru.Gelbooru, http)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<List<DapiImageObject>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
|
||||
var uri = $"http://gelbooru.com/index.php?page=dapi&s=post&json=1&q=index&limit=100" +
|
||||
$"&tags={tagString}&pid={page}";
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
|
||||
res.EnsureSuccessStatusCode();
|
||||
var resString = await res.Content.ReadAsStringAsync(cancel);
|
||||
if (string.IsNullOrWhiteSpace(resString))
|
||||
return new();
|
||||
|
||||
var images = JsonSerializer.Deserialize<List<DapiImageObject>>(resString, _serializerOptions);
|
||||
if (images is null)
|
||||
return new();
|
||||
|
||||
return images.Where(x => x.FileUrl is not null).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public interface IImageDownloader
|
||||
{
|
||||
Task<List<ImageData>> DownloadImageDataAsync(string[] tags, int page = 0,
|
||||
bool isExplicit = false, CancellationToken cancel = default);
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public abstract class ImageDownloader<T> : IImageDownloader
|
||||
where T : IImageData
|
||||
{
|
||||
protected readonly HttpClient _http;
|
||||
|
||||
protected JsonSerializerOptions _serializerOptions = new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString,
|
||||
|
||||
};
|
||||
|
||||
public Booru Booru { get; }
|
||||
|
||||
public ImageDownloader(Booru booru, HttpClient http)
|
||||
{
|
||||
_http = http;
|
||||
this.Booru = booru;
|
||||
}
|
||||
|
||||
public abstract Task<List<T>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default);
|
||||
|
||||
public async Task<List<ImageData>> DownloadImageDataAsync(string[] tags, int page, bool isExplicit = false,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var images = await DownloadImagesAsync(tags, page, isExplicit, cancel).ConfigureAwait(false);
|
||||
return images.Select(x => x.ToCachedImageData(Booru)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public static class ImageDownloaderHelper
|
||||
{
|
||||
public static string GetTagString(IEnumerable<string> tags, bool isExplicit = false)
|
||||
{
|
||||
if (isExplicit)
|
||||
tags = tags.Append("rating:explicit");
|
||||
|
||||
return string.Join('+', tags.Select(x => x.ToLowerInvariant()));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public sealed class KonachanImageDownloader : ImageDownloader<DapiImageObject>
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public KonachanImageDownloader(HttpClient http)
|
||||
: base(Booru.Konachan, http)
|
||||
{
|
||||
_baseUrl = "https://konachan.com";
|
||||
}
|
||||
|
||||
public override async Task<List<DapiImageObject>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
|
||||
var uri = $"{_baseUrl}/post.json?s=post&q=index&limit=200&tags={tagString}&page={page}";
|
||||
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel)
|
||||
.ConfigureAwait(false);
|
||||
if (imageObjects is null)
|
||||
return new();
|
||||
return imageObjects
|
||||
.Where(x => x.FileUrl is not null)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class Rule34ImageDownloader : ImageDownloader<Rule34Object>
|
||||
{
|
||||
public Rule34ImageDownloader(HttpClient http) : base(Booru.Rule34, http)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<List<Rule34Object>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags);
|
||||
var uri = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100" +
|
||||
$"&tags={tagString}&pid={page}";
|
||||
var images = await _http.GetFromJsonAsync<List<Rule34Object>>(uri, _serializerOptions, cancel).ConfigureAwait(false);
|
||||
|
||||
if (images is null)
|
||||
return new();
|
||||
|
||||
return images
|
||||
.Where(img => !string.IsNullOrWhiteSpace(img.Image))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public class SafebooruImageDownloader : ImageDownloader<SafebooruElement>
|
||||
{
|
||||
public SafebooruImageDownloader(HttpClient http) : base(Booru.Safebooru, http)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<List<SafebooruElement>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: false);
|
||||
var uri = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}";
|
||||
var images = await _http.GetFromJsonAsync<List<SafebooruElement>>(uri, _serializerOptions, cancellationToken: cancel);
|
||||
if (images is null)
|
||||
return new();
|
||||
|
||||
return images;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Nsfw.Common
|
||||
{
|
||||
public sealed class SankakuImageDownloader : ImageDownloader<SankakuImageObject>
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public SankakuImageDownloader(HttpClient http)
|
||||
: base(Booru.Sankaku, http)
|
||||
{
|
||||
_baseUrl = "https://capi-v2.sankakucomplex.com";
|
||||
_http.AddFakeHeaders();
|
||||
}
|
||||
|
||||
public override async Task<List<SankakuImageObject>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
|
||||
{
|
||||
// explicit probably not supported
|
||||
var tagString = ImageDownloaderHelper.GetTagString(tags, false);
|
||||
|
||||
var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50";
|
||||
var data = await _http.GetStringAsync(uri).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize<SankakuImageObject[]>(data, _serializerOptions)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user