mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
52
CHANGELOG.md
52
CHANGELOG.md
@@ -2,9 +2,57 @@
|
|||||||
|
|
||||||
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
## Unreleased
|
## [3.0.10] - 01.12.2021
|
||||||
|
|
||||||
## [3.0.7]
|
### 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
|
### Added
|
||||||
- `.streamsclear` re-added. It will remove all followed streams on the server.
|
- `.streamsclear` re-added. It will remove all followed streams on the server.
|
||||||
|
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Coordinator", "sr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.VotesApi", "src\NadekoBot.VotesApi\NadekoBot.VotesApi.csproj", "{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -73,6 +81,7 @@ Global
|
|||||||
{DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
{DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
{AE9B7F8C-81D7-4401-83A3-643B38258374} = {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}
|
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
|
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}
|
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]
|
echo "[Unit]
|
||||||
Description=NadekoBot service
|
Description=NadekoBot service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
StartLimitBurst=2
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
@@ -144,10 +146,11 @@ Compared to using tmux, this method requires a little bit more work to set up, b
|
|||||||
# source code.
|
# source code.
|
||||||
#ExecStartPre=/usr/bin/dotnet build ../src/NadekoBot/NadekoBot.csproj -c Release -o output/
|
#ExecStartPre=/usr/bin/dotnet build ../src/NadekoBot/NadekoBot.csproj -c Release -o output/
|
||||||
ExecStart=/usr/bin/dotnet NadekoBot.dll
|
ExecStart=/usr/bin/dotnet NadekoBot.dll
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
StandardOutput=syslog
|
StandardOutput=syslog
|
||||||
StandardError=syslog
|
StandardError=syslog
|
||||||
SyslogIdentifier=NadekoBot
|
SyslogIdentifier=NadekoBot
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target" | sudo tee /etc/systemd/system/nadeko.service
|
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]
|
echo "[Unit]
|
||||||
Description=NadekoBot service
|
Description=NadekoBot service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
StartLimitBurst=2
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=$USER
|
User=$USER
|
||||||
WorkingDirectory=$PWD
|
WorkingDirectory=$_WORKING_DIR
|
||||||
ExecStart=/bin/bash NadekoRun.sh
|
ExecStart=/bin/bash NadekoRun.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
StandardOutput=syslog
|
StandardOutput=syslog
|
||||||
StandardError=syslog
|
StandardError=syslog
|
||||||
SyslogIdentifier=NadekoBot
|
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:
|
4. Use the following command to create a script that will be used to start Nadeko:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "#\!/bin/bash
|
{
|
||||||
|
echo '#!/bin/bash'
|
||||||
echo \"\"
|
echo ""
|
||||||
echo \"Running NadekoBot in the background with auto restart\"
|
echo "echo \"Running NadekoBot in the background with auto restart\"
|
||||||
youtube-dl -U
|
youtube-dl -U
|
||||||
|
|
||||||
# If you want Nadeko to be compiled prior to every startup, uncomment the lines
|
# 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.
|
# source code.
|
||||||
#echo \"Compiling NadekoBot...\"
|
#echo \"Compiling NadekoBot...\"
|
||||||
#cd \"$PWD\"/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...\"
|
echo \"Starting NadekoBot...\"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
{
|
if [[ -d $PWD/nadekobot/output ]]; then
|
||||||
cd \"$PWD\"/nadekobot/output
|
cd $PWD/nadekobot/output || {
|
||||||
dotnet NadekoBot.dll
|
echo \"Failed to change working directory to $PWD/nadekobot/output\" >&2
|
||||||
## If a non-zero exit code is produced, exit this script.
|
echo \"Ensure that the working directory inside of '/etc/systemd/system/nadeko.service' is correct\"
|
||||||
} || {
|
echo \"Exiting...\"
|
||||||
error_code=\"\$?\"
|
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 \"An error occurred when trying to start NadekBot\"
|
||||||
echo \"EXIT CODE: \$?\"
|
echo \"Exiting...\"
|
||||||
exit \"\$error_code\"
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo \"Waiting for 5 seconds...\"
|
||||||
|
sleep 5
|
||||||
youtube-dl -U
|
youtube-dl -U
|
||||||
echo \"Restarting NadekoBot...\"
|
echo \"Restarting NadekoBot...\"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo \"Stopping NadekoBot...\"" > NadekoRun.sh
|
echo \"Stopping NadekoBot...\""
|
||||||
|
} > NadekoRun.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Start Nadeko:
|
5. Start Nadeko:
|
||||||
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
|
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
- Windows 8 or later (64-bit)
|
- 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**
|
**Optional**
|
||||||
|
|
||||||
@@ -32,12 +32,13 @@
|
|||||||
|
|
||||||
- Download and run the [NadekoBot v3 Updater][Updater].
|
- Download and run the [NadekoBot v3 Updater][Updater].
|
||||||
- Click on the + at the top left to create a new bot.
|
- 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.
|
- Give your bot a name and then click **`Go to setup`** at the lower right.
|
||||||

|

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

|

|
||||||
- Click on **`Install`** next to **`Redis`**.
|
- 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 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).
|
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
|
||||||
- When installation is finished, click on **`CREDS`** to the left of **`RUN`** at the lower right.
|
- When installation is finished, click on **`CREDS`** to the left of **`RUN`** at the lower right.
|
||||||
|
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": "*"
|
||||||
|
}
|
@@ -28,7 +28,7 @@ namespace NadekoBot
|
|||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
private readonly CommandService _commandService;
|
private readonly CommandService _commandService;
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly BotCredsProvider _credsProvider;
|
private readonly IBotCredsProvider _credsProvider;
|
||||||
|
|
||||||
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
||||||
|
|
||||||
@@ -95,8 +95,8 @@ namespace NadekoBot
|
|||||||
}
|
}
|
||||||
|
|
||||||
var svcs = new ServiceCollection()
|
var svcs = new ServiceCollection()
|
||||||
.AddTransient<IBotCredentials>(_ => _creds) // bot creds
|
.AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
|
||||||
.AddSingleton(_credsProvider)
|
.AddSingleton<IBotCredsProvider>(_credsProvider)
|
||||||
.AddSingleton(_db) // database
|
.AddSingleton(_db) // database
|
||||||
.AddRedis(_creds.RedisOptions) // redis
|
.AddRedis(_creds.RedisOptions) // redis
|
||||||
.AddSingleton(Client) // discord socket client
|
.AddSingleton(Client) // discord socket client
|
||||||
@@ -145,7 +145,8 @@ namespace NadekoBot
|
|||||||
|
|
||||||
svcs.Scan(scan => scan
|
svcs.Scan(scan => scan
|
||||||
.FromAssemblyOf<IReadyExecutor>()
|
.FromAssemblyOf<IReadyExecutor>()
|
||||||
.AddClasses(classes => classes.AssignableToAny(
|
.AddClasses(classes => classes
|
||||||
|
.AssignableToAny(
|
||||||
// services
|
// services
|
||||||
typeof(INService),
|
typeof(INService),
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Attributes
|
|||||||
{
|
{
|
||||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
|
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")));
|
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>();
|
OwnerIds = new List<ulong>();
|
||||||
TotalShards = 1;
|
TotalShards = 1;
|
||||||
GoogleApiKey = string.Empty;
|
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);
|
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||||
BotListToken = string.Empty;
|
BotListToken = string.Empty;
|
||||||
CleverbotApiKey = 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.")]
|
Change only if you've changed the coordinator address or port.")]
|
||||||
public string CoordinatorUrl { get; set; }
|
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)")]
|
[Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
|
||||||
public string RapidApiKey { get; set; }
|
public string RapidApiKey { get; set; }
|
||||||
|
|
||||||
@@ -126,11 +116,9 @@ Windows default
|
|||||||
// todo fixup patreon
|
// todo fixup patreon
|
||||||
public sealed record PatreonSettings
|
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; }
|
public string AccessToken { get; set; }
|
||||||
[Comment(@"Unused atm")]
|
|
||||||
public string RefreshToken { get; set; }
|
public string RefreshToken { get; set; }
|
||||||
[Comment(@"Unused atm")]
|
|
||||||
public string ClientSecret { get; set; }
|
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)")]
|
[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;
|
ClientSecret = clientSecret;
|
||||||
CampaignId = campaignId;
|
CampaignId = campaignId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PatreonSettings()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record VotesSettings
|
public sealed record VotesSettings
|
||||||
{
|
{
|
||||||
[Comment(@"")]
|
[Comment(@"top.gg votes service url
|
||||||
public string Url { get; set; }
|
This is the url of your instance of the NadekoBot.Votes api
|
||||||
[Comment(@"")]
|
Example: https://votes.my.cool.bot.com")]
|
||||||
public string Key { get; set; }
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,16 +12,14 @@ namespace NadekoBot
|
|||||||
string GoogleApiKey { get; }
|
string GoogleApiKey { get; }
|
||||||
ICollection<ulong> OwnerIds { get; }
|
ICollection<ulong> OwnerIds { get; }
|
||||||
string RapidApiKey { get; }
|
string RapidApiKey { get; }
|
||||||
string PatreonAccessToken { get; }
|
|
||||||
|
|
||||||
Creds.DbOptions Db { get; }
|
Creds.DbOptions Db { get; }
|
||||||
string OsuApiKey { get; }
|
string OsuApiKey { get; }
|
||||||
int TotalShards { get; }
|
int TotalShards { get; }
|
||||||
string PatreonCampaignId { get; }
|
Creds.PatreonSettings Patreon { get; }
|
||||||
string CleverbotApiKey { get; }
|
string CleverbotApiKey { get; }
|
||||||
RestartConfig RestartCommand { get; }
|
RestartConfig RestartCommand { get; }
|
||||||
string VotesUrl { get; }
|
Creds.VotesSettings Votes { get; }
|
||||||
string VotesToken { get; }
|
|
||||||
string BotListToken { get; }
|
string BotListToken { get; }
|
||||||
string RedisOptions { get; }
|
string RedisOptions { get; }
|
||||||
string LocationIqApiKey { get; }
|
string LocationIqApiKey { get; }
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using NadekoBot.Common.Yml;
|
||||||
|
|
||||||
namespace NadekoBot.Common
|
namespace NadekoBot.Common
|
||||||
{
|
{
|
||||||
public class ImageUrls
|
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 CoinData Coins { get; set; }
|
||||||
public Uri[] Currency { get; set; }
|
public Uri[] Currency { get; set; }
|
||||||
@@ -25,7 +27,6 @@ namespace NadekoBot.Common
|
|||||||
public class SlotData
|
public class SlotData
|
||||||
{
|
{
|
||||||
public Uri[] Emojis { get; set; }
|
public Uri[] Emojis { get; set; }
|
||||||
public Uri[] Numbers { get; set; }
|
|
||||||
public Uri Bg { 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -69,6 +69,7 @@ namespace NadekoBot.Common.Replacements
|
|||||||
var newF = new SmartTextEmbedField();
|
var newF = new SmartTextEmbedField();
|
||||||
newF.Name = Replace(f.Name);
|
newF.Name = Replace(f.Name);
|
||||||
newF.Value = Replace(f.Value);
|
newF.Value = Replace(f.Value);
|
||||||
|
newF.Inline = f.Inline;
|
||||||
fields.Add(newF);
|
fields.Add(newF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 Rgba32Converter())
|
||||||
.WithTypeConverter(new CultureInfoConverter())
|
.WithTypeConverter(new CultureInfoConverter())
|
||||||
.WithTypeConverter(new UriConverter())
|
.WithTypeConverter(new UriConverter())
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
using NadekoBot.Db.Models;
|
using System;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Discord;
|
using Discord;
|
||||||
@@ -138,14 +139,15 @@ WHERE UserId={userId};");
|
|||||||
// just update the amount, there is no new user data
|
// just update the amount, there is no new user data
|
||||||
if (!updatedUserData)
|
if (!updatedUserData)
|
||||||
{
|
{
|
||||||
ctx.Database.ExecuteSqlInterpolated($@"
|
var rows = ctx.Database.ExecuteSqlInterpolated($@"
|
||||||
UPDATE OR IGNORE DiscordUser
|
UPDATE OR IGNORE DiscordUser
|
||||||
SET CurrencyAmount=CurrencyAmount+{amount}
|
SET CurrencyAmount=CurrencyAmount+{amount}
|
||||||
WHERE UserId={userId};
|
WHERE UserId={userId};
|
||||||
|
|
||||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
|
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
|
||||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
|
||||||
");
|
");
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -157,8 +159,8 @@ SET CurrencyAmount=CurrencyAmount+{amount},
|
|||||||
AvatarId={avatarId}
|
AvatarId={avatarId}
|
||||||
WHERE UserId={userId};
|
WHERE UserId={userId};
|
||||||
|
|
||||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
|
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
|
||||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -167,7 +169,7 @@ VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
|||||||
public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
|
public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
|
||||||
{
|
{
|
||||||
return users
|
return users
|
||||||
.Sum(x => x.CurrencyAmount);
|
.Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
|
public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
|
||||||
|
@@ -54,7 +54,6 @@ namespace NadekoBot.Db
|
|||||||
.Include(gc => gc.CommandCooldowns)
|
.Include(gc => gc.CommandCooldowns)
|
||||||
.Include(gc => gc.FollowedStreams)
|
.Include(gc => gc.FollowedStreams)
|
||||||
.Include(gc => gc.StreamRole)
|
.Include(gc => gc.StreamRole)
|
||||||
.Include(gc => gc.NsfwBlacklistedTags)
|
|
||||||
.Include(gc => gc.XpSettings)
|
.Include(gc => gc.XpSettings)
|
||||||
.ThenInclude(x => x.ExclusionList)
|
.ThenInclude(x => x.ExclusionList)
|
||||||
.Include(gc => gc.DelMsgOnCmdChannels)
|
.Include(gc => gc.DelMsgOnCmdChannels)
|
||||||
|
@@ -91,7 +91,6 @@ namespace NadekoBot.Services.Database.Models
|
|||||||
public bool WarningsInitialized { get; set; }
|
public bool WarningsInitialized { get; set; }
|
||||||
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
||||||
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
|
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
|
||||||
public HashSet<NsfwBlacklitedTag> NsfwBlacklistedTags { get; set; } = new HashSet<NsfwBlacklitedTag>();
|
|
||||||
|
|
||||||
public List<ShopEntry> ShopEntries { get; set; }
|
public List<ShopEntry> ShopEntries { get; set; }
|
||||||
public ulong? GameVoiceChannel { get; set; } = null;
|
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 bool Forgiven { get; set; }
|
||||||
public string ForgivenBy { get; set; }
|
public string ForgivenBy { get; set; }
|
||||||
public string Moderator { get; set; }
|
public string Moderator { get; set; }
|
||||||
|
public int Weight { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -59,6 +59,7 @@ namespace NadekoBot.Services.Database
|
|||||||
public DbSet<Poll> Poll { get; set; }
|
public DbSet<Poll> Poll { get; set; }
|
||||||
public DbSet<WaifuInfo> WaifuInfo { get; set; }
|
public DbSet<WaifuInfo> WaifuInfo { get; set; }
|
||||||
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
|
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
|
||||||
|
public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
|
||||||
|
|
||||||
public NadekoContext(DbContextOptions<NadekoContext> options) : base(options)
|
public NadekoContext(DbContextOptions<NadekoContext> options) : base(options)
|
||||||
{
|
{
|
||||||
@@ -195,10 +196,16 @@ namespace NadekoBot.Services.Database
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Warnings
|
#region Warnings
|
||||||
var warn = modelBuilder.Entity<Warning>();
|
|
||||||
warn.HasIndex(x => x.GuildId);
|
modelBuilder.Entity<Warning>(warn =>
|
||||||
warn.HasIndex(x => x.UserId);
|
{
|
||||||
warn.HasIndex(x => x.DateAdded);
|
warn.HasIndex(x => x.GuildId);
|
||||||
|
warn.HasIndex(x => x.UserId);
|
||||||
|
warn.HasIndex(x => x.DateAdded);
|
||||||
|
warn.Property(x => x.Weight)
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
});
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region PatreonRewards
|
#region PatreonRewards
|
||||||
@@ -357,6 +364,10 @@ namespace NadekoBot.Services.Database
|
|||||||
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc
|
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc
|
||||||
.HasIndex(x => x.ChannelId)
|
.HasIndex(x => x.ChannelId)
|
||||||
.IsUnique());
|
.IsUnique());
|
||||||
|
|
||||||
|
modelBuilder.Entity<NsfwBlacklistedTag>(nbt => nbt
|
||||||
|
.HasIndex(x => x.GuildId)
|
||||||
|
.IsUnique(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,12 @@ namespace NadekoBot.Migrations
|
|||||||
migrationBuilder.Sql("DELETE FROM FilterChannelId WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
|
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)");
|
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(
|
migrationBuilder.DropColumn(
|
||||||
name: "ChannelCreated",
|
name: "ChannelCreated",
|
||||||
table: "LogSettings");
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1062,7 +1062,7 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("MutedUserId");
|
b.ToTable("MutedUserId");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
|
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -1071,7 +1071,7 @@ namespace NadekoBot.Migrations
|
|||||||
b.Property<DateTime?>("DateAdded")
|
b.Property<DateTime?>("DateAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("GuildConfigId")
|
b.Property<ulong>("GuildId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Tag")
|
b.Property<string>("Tag")
|
||||||
@@ -1079,9 +1079,9 @@ namespace NadekoBot.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("GuildConfigId");
|
b.HasIndex("GuildId");
|
||||||
|
|
||||||
b.ToTable("NsfwBlacklitedTag");
|
b.ToTable("NsfwBlacklistedTags");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
||||||
@@ -1967,6 +1967,11 @@ namespace NadekoBot.Migrations
|
|||||||
b.Property<ulong>("UserId")
|
b.Property<ulong>("UserId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("DateAdded");
|
b.HasIndex("DateAdded");
|
||||||
@@ -2300,13 +2305,6 @@ namespace NadekoBot.Migrations
|
|||||||
.HasForeignKey("GuildConfigId");
|
.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 =>
|
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
||||||
@@ -2573,8 +2571,6 @@ namespace NadekoBot.Migrations
|
|||||||
|
|
||||||
b.Navigation("MutedUsers");
|
b.Navigation("MutedUsers");
|
||||||
|
|
||||||
b.Navigation("NsfwBlacklistedTags");
|
|
||||||
|
|
||||||
b.Navigation("Permissions");
|
b.Navigation("Permissions");
|
||||||
|
|
||||||
b.Navigation("ReactionRoleMessages");
|
b.Navigation("ReactionRoleMessages");
|
||||||
|
@@ -12,6 +12,8 @@ using LinqToDB;
|
|||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using System.Threading;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Administration.Services
|
namespace NadekoBot.Modules.Administration.Services
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,11 @@ namespace NadekoBot.Modules.Administration.Services
|
|||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
|
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,
|
public RoleCommandsService(DiscordSocketClient client, DbService db,
|
||||||
Bot bot)
|
Bot bot)
|
||||||
{
|
{
|
||||||
@@ -38,75 +45,58 @@ namespace NadekoBot.Modules.Administration.Services
|
|||||||
|
|
||||||
private Task _client_ReactionAdded(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
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 ||
|
if (!conf.Exclusive)
|
||||||
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)
|
await AddReactionRoleAsync(gusr, reactionRole);
|
||||||
{
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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);
|
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg, gusr, reaction, conf, reactionRole, CancellationToken.None);
|
||||||
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
|
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
|
||||||
new RequestOptions()
|
|
||||||
{
|
await Task.WhenAll(removeExclusiveTask, addRoleTask).ConfigureAwait(false);
|
||||||
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
|
}
|
||||||
}).ConfigureAwait(false);
|
finally
|
||||||
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
|
{
|
||||||
|
// 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;
|
return Task.CompletedTask;
|
||||||
@@ -114,16 +104,16 @@ namespace NadekoBot.Modules.Administration.Services
|
|||||||
|
|
||||||
private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
||||||
{
|
{
|
||||||
var _ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!reaction.User.IsSpecified ||
|
if (!reaction.User.IsSpecified ||
|
||||||
reaction.User.Value.IsBot ||
|
reaction.User.Value.IsBot ||
|
||||||
!(reaction.User.Value is SocketGuildUser gusr))
|
reaction.User.Value is not SocketGuildUser gusr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!(chan is SocketGuildChannel gch))
|
if (chan is not SocketGuildChannel gch)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
|
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||||
@@ -193,5 +183,71 @@ namespace NadekoBot.Modules.Administration.Services
|
|||||||
uow.SaveChanges();
|
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));
|
}, 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();
|
var modName = mod.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(reason))
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
@@ -57,6 +60,7 @@ namespace NadekoBot.Modules.Administration.Services
|
|||||||
Forgiven = false,
|
Forgiven = false,
|
||||||
Reason = reason,
|
Reason = reason,
|
||||||
Moderator = modName,
|
Moderator = modName,
|
||||||
|
Weight = weight,
|
||||||
};
|
};
|
||||||
|
|
||||||
int warnings = 1;
|
int warnings = 1;
|
||||||
@@ -70,7 +74,7 @@ namespace NadekoBot.Modules.Administration.Services
|
|||||||
.Warnings
|
.Warnings
|
||||||
.ForId(guildId, userId)
|
.ForId(guildId, userId)
|
||||||
.Where(w => !w.Forgiven && w.UserId == userId)
|
.Where(w => !w.Forgiven && w.UserId == userId)
|
||||||
.Count();
|
.Sum(x => x.Weight);
|
||||||
|
|
||||||
uow.Warnings.Add(warn);
|
uow.Warnings.Add(warn);
|
||||||
|
|
||||||
|
@@ -54,8 +54,17 @@ namespace NadekoBot.Modules.Administration
|
|||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.BanMembers)]
|
[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))
|
if (!await CheckRoleHierarchy(user))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -76,7 +85,7 @@ namespace NadekoBot.Modules.Administration
|
|||||||
WarningPunishment punishment;
|
WarningPunishment punishment;
|
||||||
try
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -230,19 +239,29 @@ namespace NadekoBot.Modules.Administration
|
|||||||
}
|
}
|
||||||
else
|
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;
|
var i = page * 9;
|
||||||
foreach (var w in warnings)
|
foreach (var w in warnings)
|
||||||
{
|
{
|
||||||
i++;
|
i++;
|
||||||
var name = GetText(strs.warned_on_by(
|
var name = GetText(strs.warned_on_by(
|
||||||
w.DateAdded.Value.ToString("dd.MM.yyy"),
|
w.DateAdded?.ToString("dd.MM.yyy"),
|
||||||
w.DateAdded.Value.ToString("HH:mm"),
|
w.DateAdded?.ToString("HH:mm"),
|
||||||
w.Moderator));
|
w.Moderator));
|
||||||
|
|
||||||
if (w.Forgiven)
|
if (w.Forgiven)
|
||||||
name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
|
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()
|
var toSend = _eb.Create().WithOkColor()
|
||||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
.AddField(GetText(strs.username), user.ToString(), true)
|
.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);
|
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||||
if (user is null)
|
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()
|
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
@@ -516,7 +535,7 @@ namespace NadekoBot.Modules.Administration
|
|||||||
dmFailed = true;
|
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()
|
var toSend = _eb.Create().WithOkColor()
|
||||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
@@ -692,7 +711,7 @@ namespace NadekoBot.Modules.Administration
|
|||||||
dmFailed = true;
|
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); }
|
try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
|
||||||
catch { 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;
|
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()
|
var toSend = _eb.Create().WithOkColor()
|
||||||
.WithTitle(GetText(strs.kicked_user))
|
.WithTitle(GetText(strs.kicked_user))
|
||||||
@@ -776,23 +795,32 @@ namespace NadekoBot.Modules.Administration
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var missing = new List<string>();
|
var missing = new List<string>();
|
||||||
var banning = new HashSet<IGuildUser>();
|
var banning = new HashSet<IUser>();
|
||||||
|
|
||||||
await ctx.Channel.TriggerTypingAsync();
|
await ctx.Channel.TriggerTypingAsync();
|
||||||
foreach (var userStr in userStrings)
|
foreach (var userStr in userStrings)
|
||||||
{
|
{
|
||||||
if (ulong.TryParse(userStr, out var userId))
|
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);
|
await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
missing.Add(userStr);
|
// if IGuildUser is null, try to get IUser
|
||||||
continue;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -820,7 +848,7 @@ namespace NadekoBot.Modules.Administration
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await toBan.BanAsync(7);
|
await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@@ -149,7 +149,7 @@ namespace NadekoBot.Modules.CustomReactions
|
|||||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||||
.WithDescription($"#{id}")
|
.WithDescription($"#{id}")
|
||||||
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
|
.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);
|
).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ namespace NadekoBot.Modules.Gambling.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Comment(@"DO NOT CHANGE")]
|
[Comment(@"DO NOT CHANGE")]
|
||||||
public int Version { get; set; } = 1;
|
public int Version { get; set; } = 2;
|
||||||
|
|
||||||
[Comment(@"Currency settings")]
|
[Comment(@"Currency settings")]
|
||||||
public CurrencyConfig Currency { get; set; }
|
public CurrencyConfig Currency { get; set; }
|
||||||
@@ -60,6 +60,10 @@ Set 0 for unlimited")]
|
|||||||
[Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
|
[Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
|
||||||
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
|
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
|
||||||
public decimal PatreonCurrencyPerCent { get; set; } = 1;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CurrencyConfig
|
public class CurrencyConfig
|
||||||
|
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 IImageCache _images;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly DbService _db;
|
|
||||||
private static readonly NadekoRandom rng = new NadekoRandom();
|
private static readonly NadekoRandom rng = new NadekoRandom();
|
||||||
|
|
||||||
public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db,
|
public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss)
|
||||||
GamblingConfigService gss) : base(gss)
|
|
||||||
{
|
{
|
||||||
_images = data.LocalImages;
|
_images = data.LocalImages;
|
||||||
_cs = cs;
|
_cs = cs;
|
||||||
_db = db;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
|
@@ -66,11 +66,11 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
}
|
}
|
||||||
var embed = _eb.Create()
|
var embed = _eb.Create()
|
||||||
.WithTitle(GetText(strs.economy_state))
|
.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_one_percent), (onePercent * 100).ToString("F2") + "%")
|
||||||
.AddField(GetText(strs.currency_planted), ((BigInteger)ec.Planted) + CurrencySign)
|
.AddField(GetText(strs.currency_planted), ((BigInteger)ec.Planted) + CurrencySign)
|
||||||
.AddField(GetText(strs.owned_waifus_total), ((BigInteger)ec.Waifus) + 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)
|
.AddField(GetText(strs.total), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
|
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
|
||||||
@@ -247,20 +247,20 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
[Priority(0)]
|
[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);
|
Award(amount, usr.Id, msg);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
[Priority(1)]
|
[Priority(1)]
|
||||||
public Task Award(ShmartNumber amount, [Leftover] IGuildUser usr) =>
|
public Task Award(long amount, [Leftover] IGuildUser usr) =>
|
||||||
Award(amount, usr.Id);
|
Award(amount, usr.Id);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
[Priority(2)]
|
[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)
|
if (amount <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -276,7 +276,7 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
[Priority(2)]
|
[Priority(2)]
|
||||||
public async Task Award(ShmartNumber amount, [Leftover] IRole role)
|
public async Task Award(long amount, [Leftover] IRole role)
|
||||||
{
|
{
|
||||||
var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false))
|
var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false))
|
||||||
.Where(u => u.GetRoles().Contains(role))
|
.Where(u => u.GetRoles().Contains(role))
|
||||||
@@ -284,7 +284,7 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
|
|
||||||
await _cs.AddBulkAsync(users.Select(x => x.Id),
|
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 => $"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)
|
gamble: true)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -298,13 +298,13 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
[Priority(0)]
|
[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();
|
var users = (await role.GetMembersAsync()).ToList();
|
||||||
|
|
||||||
await _cs.RemoveBulkAsync(users.Select(x => x.Id),
|
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 => $"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)
|
gamble: true)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
[Priority(1)]
|
[Priority(1)]
|
||||||
public async Task Take(ShmartNumber amount, [Leftover] IGuildUser user)
|
public async Task Take(long amount, [Leftover] IGuildUser user)
|
||||||
{
|
{
|
||||||
if (amount <= 0)
|
if (amount <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -333,7 +333,7 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
public async Task Take(ShmartNumber amount, [Leftover] ulong usrId)
|
public async Task Take(long amount, [Leftover] ulong usrId)
|
||||||
{
|
{
|
||||||
if (amount <= 0)
|
if (amount <= 0)
|
||||||
return;
|
return;
|
||||||
|
@@ -8,10 +8,11 @@ using NadekoBot.Modules.Gambling.Services;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
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]
|
[Group]
|
||||||
public class PlantPickCommands : GamblingSubmodule<PlantPickService>
|
public class PlantPickCommands : GamblingSubmodule<PlantPickService>
|
||||||
@@ -53,7 +54,7 @@ namespace NadekoBot.Modules.Games
|
|||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireContext(ContextType.Guild)]
|
[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)
|
if (amount < 1)
|
||||||
return;
|
return;
|
||||||
@@ -63,18 +64,17 @@ namespace NadekoBot.Modules.Games
|
|||||||
return;
|
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)
|
if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
|
||||||
{
|
{
|
||||||
logService.AddDeleteIgnore(ctx.Message.Id);
|
logService.AddDeleteIgnore(ctx.Message.Id);
|
||||||
await ctx.Message.DeleteAsync().ConfigureAwait(false);
|
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]
|
[NadekoCommand, Aliases]
|
@@ -8,8 +8,6 @@ using System.Threading.Tasks;
|
|||||||
using System;
|
using System;
|
||||||
using NadekoBot.Services.Database.Models;
|
using NadekoBot.Services.Database.Models;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Linq;
|
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@@ -17,76 +15,22 @@ namespace NadekoBot.Modules.Gambling.Services
|
|||||||
{
|
{
|
||||||
public class CurrencyEventsService : INService
|
public class CurrencyEventsService : INService
|
||||||
{
|
{
|
||||||
public class VoteModel
|
|
||||||
{
|
|
||||||
public ulong User { get; set; }
|
|
||||||
public long Date { get; set; }
|
|
||||||
}
|
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IBotCredentials _creds;
|
|
||||||
private readonly IHttpClientFactory _http;
|
|
||||||
private readonly GamblingConfigService _configService;
|
private readonly GamblingConfigService _configService;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
|
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
|
||||||
new ConcurrentDictionary<ulong, ICurrencyEvent>();
|
new ConcurrentDictionary<ulong, ICurrencyEvent>();
|
||||||
|
|
||||||
public CurrencyEventsService(DiscordSocketClient client,
|
|
||||||
IBotCredentials creds, ICurrencyService cs,
|
public CurrencyEventsService(
|
||||||
IHttpClientFactory http, GamblingConfigService configService)
|
DiscordSocketClient client,
|
||||||
|
ICurrencyService cs,
|
||||||
|
GamblingConfigService configService)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_cs = cs;
|
_cs = cs;
|
||||||
_creds = creds;
|
|
||||||
_http = http;
|
|
||||||
_configService = configService;
|
_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,
|
public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
|
||||||
@@ -127,6 +71,7 @@ namespace NadekoBot.Modules.Gambling.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,4 +81,4 @@ namespace NadekoBot.Modules.Gambling.Services
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -63,6 +63,14 @@ namespace NadekoBot.Modules.Gambling.Services
|
|||||||
c.Version = 2;
|
c.Version = 2;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_data.Version < 3)
|
||||||
|
{
|
||||||
|
ModifyConfig(c =>
|
||||||
|
{
|
||||||
|
c.VoteReward = 100;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,12 +6,14 @@ using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NadekoBot.Common;
|
using NadekoBot.Common;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
|
using NadekoBot.Modules.Gambling.Common.Slot;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@@ -82,6 +84,41 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
|
|||||||
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
}, 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
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -9,12 +9,16 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NadekoBot.Common;
|
using NadekoBot.Common;
|
||||||
using NadekoBot.Common.Attributes;
|
using NadekoBot.Common.Attributes;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
using NadekoBot.Services;
|
using NadekoBot.Services;
|
||||||
|
using SixLabors.Fonts;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Gambling
|
namespace NadekoBot.Modules.Gambling
|
||||||
{
|
{
|
||||||
@@ -33,12 +37,16 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
//thanks to judge for helping me with this
|
//thanks to judge for helping me with this
|
||||||
|
|
||||||
private readonly IImageCache _images;
|
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;
|
_images = data.LocalImages;
|
||||||
_cs = cs;
|
_fonts = fonts;
|
||||||
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SlotMachine
|
public sealed class SlotMachine
|
||||||
@@ -140,92 +148,115 @@ namespace NadekoBot.Modules.Gambling
|
|||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
public async Task Slot(ShmartNumber amount)
|
public async Task Slot(ShmartNumber amount)
|
||||||
{
|
{
|
||||||
if (!_runningUsers.Add(ctx.User.Id))
|
if (!_runningUsers.Add(ctx.User.Id))
|
||||||
return;
|
return;
|
||||||
try
|
|
||||||
{
|
try
|
||||||
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
|
{
|
||||||
return;
|
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
|
||||||
const int maxAmount = 9999;
|
return;
|
||||||
if (amount > maxAmount)
|
|
||||||
{
|
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||||
await ReplyErrorLocalizedAsync(strs.max_bet_limit(maxAmount + CurrencySign));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false))
|
var result = await _service.SlotAsync(ctx.User.Id, amount);
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
if (result.Error != GamblingError.None)
|
||||||
{
|
{
|
||||||
using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]]))
|
if (result.Error == GamblingError.NotEnough)
|
||||||
{
|
{
|
||||||
bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions()));
|
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var won = amount * result.Multiplier;
|
return;
|
||||||
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);
|
|
||||||
|
|
||||||
var printAmount = amount;
|
Interlocked.Add(ref _totalBet, amount);
|
||||||
n = 0;
|
Interlocked.Add(ref _totalPaidOut, result.Won);
|
||||||
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);
|
|
||||||
|
|
||||||
var msg = GetText(strs.better_luck);
|
long ownedAmount;
|
||||||
if (result.Multiplier != 0)
|
using (var uow = _db.GetDbContext())
|
||||||
{
|
{
|
||||||
await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false);
|
ownedAmount = uow.Set<DiscordUser>()
|
||||||
Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
|
.FirstOrDefault(x => x.UserId == ctx.User.Id)
|
||||||
if (result.Multiplier == 1)
|
?.CurrencyAmount ?? 0;
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var imgStream = bgImage.ToStream())
|
using (var bgImage = Image.Load<Rgba32>(_images.SlotBackground, out var format))
|
||||||
{
|
{
|
||||||
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);
|
var numbers = new int[3];
|
||||||
}
|
result.Rolls.CopyTo(numbers, 0);
|
||||||
}
|
|
||||||
}
|
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||||
finally
|
{
|
||||||
{
|
TextOptions = new TextOptions()
|
||||||
var _ = Task.Run(async () =>
|
{
|
||||||
{
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
await Task.Delay(1000).ConfigureAwait(false);
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
_runningUsers.Remove(ctx.User.Id);
|
WrapTextWidth = 140,
|
||||||
});
|
}
|
||||||
}
|
}, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), SixLabors.ImageSharp.Color.Red,
|
||||||
|
new PointF(227, 92)));
|
||||||
|
|
||||||
|
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||||
|
{
|
||||||
|
TextOptions = new TextOptions()
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
WrapTextWidth = 135,
|
||||||
|
}
|
||||||
|
}, amount.ToString(), _fonts.DottyFont.CreateFont(50), SixLabors.ImageSharp.Color.Red,
|
||||||
|
new PointF(129, 472)));
|
||||||
|
|
||||||
|
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||||
|
{
|
||||||
|
TextOptions = new TextOptions()
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
WrapTextWidth = 135,
|
||||||
|
}
|
||||||
|
}, ownedAmount.ToString(), _fonts.DottyFont.CreateFont(50), SixLabors.ImageSharp.Color.Red,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
using Discord.Commands;
|
using Discord.Commands;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using NadekoBot.Common;
|
||||||
using NadekoBot.Common.Attributes;
|
using NadekoBot.Common.Attributes;
|
||||||
using NadekoBot.Services;
|
using NadekoBot.Services;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
@@ -23,6 +24,7 @@ namespace NadekoBot.Modules.Games
|
|||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NoPublicBot]
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
|
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.Directory) && !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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
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 sealed class YandereImageDownloader : ImageDownloader<DapiImageObject>
|
||||||
|
{
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public YandereImageDownloader(HttpClient http)
|
||||||
|
: base(Booru.Yandere, http)
|
||||||
|
{
|
||||||
|
_baseUrl = "https://yande.re";
|
||||||
|
}
|
||||||
|
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?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/NadekoBot/Modules/Nsfw/Common/E621Object.cs
Normal file
27
src/NadekoBot/Modules/Nsfw/Common/E621Object.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public class E621Object : IImageData
|
||||||
|
{
|
||||||
|
public class FileData
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagData
|
||||||
|
{
|
||||||
|
public string[] General { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScoreData
|
||||||
|
{
|
||||||
|
public int Total { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileData File { get; set; }
|
||||||
|
public TagData Tags { get; set; }
|
||||||
|
public ScoreData Score { get; set; }
|
||||||
|
|
||||||
|
public ImageData ToCachedImageData(Booru type)
|
||||||
|
=> new(File.Url, Booru.E621, Tags.General, Score.Total.ToString());
|
||||||
|
}
|
||||||
|
}
|
7
src/NadekoBot/Modules/Nsfw/Common/IImageData.cs
Normal file
7
src/NadekoBot/Modules/Nsfw/Common/IImageData.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public interface IImageData
|
||||||
|
{
|
||||||
|
ImageData ToCachedImageData(Booru type);
|
||||||
|
}
|
||||||
|
}
|
43
src/NadekoBot/Modules/Nsfw/Common/ImageData.cs
Normal file
43
src/NadekoBot/Modules/Nsfw/Common/ImageData.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public class ImageData : IComparable<ImageData>
|
||||||
|
{
|
||||||
|
public Booru SearchType { get; }
|
||||||
|
public string FileUrl { get; }
|
||||||
|
public HashSet<string> Tags { get; }
|
||||||
|
public string Rating { get; }
|
||||||
|
|
||||||
|
public ImageData(string url, Booru type, string[] tags, string rating)
|
||||||
|
{
|
||||||
|
if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||||
|
{
|
||||||
|
this.FileUrl = "https://danbooru.donmai.us" + url;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.SearchType = type;
|
||||||
|
this.FileUrl = url;
|
||||||
|
this.Tags = tags.ToHashSet();
|
||||||
|
this.Rating = rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return FileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() => FileUrl.GetHashCode();
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
=> obj is ImageData ico && ico.FileUrl == this.FileUrl;
|
||||||
|
|
||||||
|
public int CompareTo(ImageData other)
|
||||||
|
=> string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
19
src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs
Normal file
19
src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public class Rule34Object : IImageData
|
||||||
|
{
|
||||||
|
public string Image { get; init; }
|
||||||
|
public string Directory { get; init; }
|
||||||
|
public string Tags { get; init; }
|
||||||
|
public int Score { get; init; }
|
||||||
|
|
||||||
|
public ImageData ToCachedImageData(Booru type)
|
||||||
|
{
|
||||||
|
return new ImageData(
|
||||||
|
$"https://img.rule34.xxx//images/{Directory}/{Image}",
|
||||||
|
Booru.Rule34,
|
||||||
|
Tags.Split(' '),
|
||||||
|
Score.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs
Normal file
15
src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public class SafebooruElement : IImageData
|
||||||
|
{
|
||||||
|
public string Directory { get; set; }
|
||||||
|
public string Image { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}";
|
||||||
|
public string Rating { get; set; }
|
||||||
|
public string Tags { get; set; }
|
||||||
|
public ImageData ToCachedImageData(Booru type)
|
||||||
|
=> new ImageData(FileUrl, Booru.Safebooru, this.Tags.Split(' '), Rating);
|
||||||
|
}
|
||||||
|
}
|
27
src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs
Normal file
27
src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public class SankakuImageObject : IImageData
|
||||||
|
{
|
||||||
|
public class Tag
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("file_url")]
|
||||||
|
public string FileUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("file_type")]
|
||||||
|
public string FileType { get; set; }
|
||||||
|
|
||||||
|
public Tag[] Tags { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_score")]
|
||||||
|
public int Score { get; set; }
|
||||||
|
|
||||||
|
public ImageData ToCachedImageData(Nsfw.Common.Booru type)
|
||||||
|
=> new(FileUrl, Nsfw.Common.Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString());
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +0,0 @@
|
|||||||
namespace NadekoBot.Modules.NSFW.Exceptions
|
|
||||||
{
|
|
||||||
}
|
|
31
src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs
Normal file
31
src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NadekoBot.Modules.Nsfw.Common;
|
||||||
|
using NadekoBot.Modules.Searches.Common;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw
|
||||||
|
{
|
||||||
|
public interface ISearchImagesService
|
||||||
|
{
|
||||||
|
Task<UrlReply> Gelbooru(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Danbooru(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Konachan(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Yandere(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Rule34(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> E621(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Sankaku(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> SafeBooru(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Hentai(ulong? guildId, bool forceExplicit, string[] tags);
|
||||||
|
Task<UrlReply> Boobs();
|
||||||
|
ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag);
|
||||||
|
ValueTask<string[]> GetBlacklistedTags(ulong guildId);
|
||||||
|
Task<UrlReply> Butts();
|
||||||
|
Task<Gallery> GetNhentaiByIdAsync(uint id);
|
||||||
|
Task<Gallery> GetNhentaiBySearchAsync(string search);
|
||||||
|
ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; }
|
||||||
|
ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; }
|
||||||
|
ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; }
|
||||||
|
}
|
||||||
|
}
|
@@ -5,68 +5,28 @@ using NadekoBot.Common.Attributes;
|
|||||||
using NadekoBot.Common.Collections;
|
using NadekoBot.Common.Collections;
|
||||||
using NadekoBot.Extensions;
|
using NadekoBot.Extensions;
|
||||||
using NadekoBot.Modules.Searches.Common;
|
using NadekoBot.Modules.Searches.Common;
|
||||||
using NadekoBot.Modules.Searches.Services;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.NSFW
|
namespace NadekoBot.Modules.Nsfw
|
||||||
{
|
{
|
||||||
// thanks to halitalf for adding autoboob and autobutt features :D
|
[NoPublicBot]
|
||||||
public class NSFW : NadekoModule<SearchesService>
|
public class NSFW : NadekoModule<ISearchImagesService>
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
|
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly NadekoRandom _rng;
|
||||||
|
|
||||||
public NSFW(IHttpClientFactory factory)
|
public NSFW(IHttpClientFactory factory)
|
||||||
{
|
{
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
}
|
_rng = new NadekoRandom();
|
||||||
|
|
||||||
private async Task InternalHentai(IMessageChannel channel, string tag)
|
|
||||||
{
|
|
||||||
// create a random number generator
|
|
||||||
var rng = new NadekoRandom();
|
|
||||||
|
|
||||||
// get all of the DAPI search types, except first 3
|
|
||||||
// which are safebooru (not nsfw), and 2 furry ones 🤢
|
|
||||||
var listOfProviders = Enum.GetValues(typeof(DapiSearchType))
|
|
||||||
.Cast<DapiSearchType>()
|
|
||||||
.Skip(3)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// now try to get an image, if it fails return an error,
|
|
||||||
// keep trying for each provider until one of them is successful, or until
|
|
||||||
// we run out of providers. If we run out, then return an error
|
|
||||||
ImageCacherObject img;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
// random index of the providers
|
|
||||||
var num = rng.Next(0, listOfProviders.Count);
|
|
||||||
// get the type
|
|
||||||
var type = listOfProviders[num];
|
|
||||||
// remove it
|
|
||||||
listOfProviders.RemoveAt(num);
|
|
||||||
// get the image
|
|
||||||
img = await _service.DapiSearch(tag, type, ctx.Guild?.Id, true).ConfigureAwait(false);
|
|
||||||
// if i can't find the image, ran out of providers, or tag is blacklisted
|
|
||||||
// return the error
|
|
||||||
if (img is null && !listOfProviders.Any())
|
|
||||||
{
|
|
||||||
await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
} while (img is null);
|
|
||||||
|
|
||||||
await channel.EmbedAsync(_eb.Create().WithOkColor()
|
|
||||||
.WithImageUrl(img.FileUrl)
|
|
||||||
.WithDescription($"[{GetText(strs.tag)}: {tag}]({img})"))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InternalBoobs()
|
private async Task InternalBoobs()
|
||||||
@@ -76,8 +36,11 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
JToken obj;
|
JToken obj;
|
||||||
using (var http = _httpFactory.CreateClient())
|
using (var http = _httpFactory.CreateClient())
|
||||||
{
|
{
|
||||||
obj = JArray.Parse(await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0];
|
obj = JArray.Parse(await http
|
||||||
|
.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}")
|
||||||
|
.ConfigureAwait(false))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
|
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -85,6 +48,7 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
await SendErrorAsync(ex.Message).ConfigureAwait(false);
|
await SendErrorAsync(ex.Message).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InternalButts(IMessageChannel Channel)
|
private async Task InternalButts(IMessageChannel Channel)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -92,8 +56,11 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
JToken obj;
|
JToken obj;
|
||||||
using (var http = _httpFactory.CreateClient())
|
using (var http = _httpFactory.CreateClient())
|
||||||
{
|
{
|
||||||
obj = JArray.Parse(await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0];
|
obj = JArray.Parse(await http
|
||||||
|
.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}")
|
||||||
|
.ConfigureAwait(false))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
|
await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -102,12 +69,11 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !GLOBAL_NADEKO
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw]
|
[RequireNsfw]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(ChannelPerm.ManageMessages)]
|
[UserPerm(ChannelPerm.ManageMessages)]
|
||||||
public async Task AutoHentai(int interval = 0, string tags = null)
|
public async Task AutoHentai(int interval = 0, [Leftover] string tags = null)
|
||||||
{
|
{
|
||||||
Timer t;
|
Timer t;
|
||||||
|
|
||||||
@@ -123,16 +89,18 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
if (interval < 20)
|
if (interval < 20)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var tagsArr = tags?.Split('|');
|
|
||||||
|
|
||||||
t = new Timer(async (state) =>
|
t = new Timer(async (state) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (tagsArr is null || tagsArr.Length == 0)
|
if (tags is null || tags.Length == 0)
|
||||||
await InternalHentai(ctx.Channel, null).ConfigureAwait(false);
|
await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
await InternalHentai(ctx.Channel, tagsArr[new NadekoRandom().Next(0, tagsArr.Length)]).ConfigureAwait(false);
|
{
|
||||||
|
var groups = tags.Split('|');
|
||||||
|
var group = groups[_rng.Next(0, groups.Length)];
|
||||||
|
await InternalDapiCommand(group.Split(' '), true, _service.Hentai).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -148,7 +116,7 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
|
|
||||||
await ReplyConfirmLocalizedAsync(strs.autohentai_started(
|
await ReplyConfirmLocalizedAsync(strs.autohentai_started(
|
||||||
interval,
|
interval,
|
||||||
string.Join(", ", tagsArr)));
|
string.Join(", ", tags)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
@@ -231,25 +199,24 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
|
|
||||||
await ReplyConfirmLocalizedAsync(strs.started(interval));
|
await ReplyConfirmLocalizedAsync(strs.started(interval));
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Hentai([Leftover] string tag = null) =>
|
public Task Hentai(params string[] tags)
|
||||||
InternalHentai(ctx.Channel, tag);
|
=> InternalDapiCommand(tags, true, _service.Hentai);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public async Task HentaiBomb([Leftover] string tag = null)
|
public async Task HentaiBomb(params string[] tags)
|
||||||
{
|
{
|
||||||
if (!_hentaiBombBlacklist.Add(ctx.Guild?.Id ?? ctx.User.Id))
|
if (!_hentaiBombBlacklist.Add(ctx.Guild?.Id ?? ctx.User.Id))
|
||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var images = await Task.WhenAll(_service.DapiSearch(tag, DapiSearchType.Gelbooru, ctx.Guild?.Id, true),
|
var images = await Task.WhenAll(_service.Yandere(ctx.Guild?.Id, true, tags),
|
||||||
_service.DapiSearch(tag, DapiSearchType.Danbooru, ctx.Guild?.Id, true),
|
_service.Danbooru(ctx.Guild?.Id, true, tags),
|
||||||
_service.DapiSearch(tag, DapiSearchType.Konachan, ctx.Guild?.Id, true),
|
_service.Konachan(ctx.Guild?.Id, true, tags),
|
||||||
_service.DapiSearch(tag, DapiSearchType.Yandere, ctx.Guild?.Id, true)).ConfigureAwait(false);
|
_service.Gelbooru(ctx.Guild?.Id, true, tags));
|
||||||
|
|
||||||
var linksEnum = images?.Where(l => l != null).ToArray();
|
var linksEnum = images?.Where(l => l != null).ToArray();
|
||||||
if (images is null || !linksEnum.Any())
|
if (images is null || !linksEnum.Any())
|
||||||
@@ -258,7 +225,8 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.FileUrl))).ConfigureAwait(false);
|
await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url)))
|
||||||
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -268,43 +236,48 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Yandere([Leftover] string tag = null)
|
public Task Yandere(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Yandere, false);
|
=> InternalDapiCommand(tags, false, _service.Yandere);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Konachan([Leftover] string tag = null)
|
public Task Konachan(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Konachan, false);
|
=> InternalDapiCommand(tags, false, _service.Konachan);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
|
||||||
public Task Sankaku([Leftover] string tag = null)
|
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Sankaku, false);
|
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task E621([Leftover] string tag = null)
|
public Task Sankaku(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.E621, false);
|
=> InternalDapiCommand(tags, false, _service.Sankaku);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Rule34([Leftover] string tag = null)
|
public Task E621(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Rule34, false);
|
=> InternalDapiCommand(tags, false, _service.E621);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Danbooru([Leftover] string tag = null)
|
public Task Rule34(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Danbooru, false);
|
=> InternalDapiCommand(tags, false, _service.Rule34);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Gelbooru([Leftover] string tag = null)
|
public Task Danbooru(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Gelbooru, false);
|
=> InternalDapiCommand(tags, false, _service.Danbooru);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
public Task Derpibooru([Leftover] string tag = null)
|
public Task Gelbooru(params string[] tags)
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Derpibooru, false);
|
=> InternalDapiCommand(tags, false, _service.Gelbooru);
|
||||||
|
|
||||||
|
[NadekoCommand, Aliases]
|
||||||
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
|
public Task Derpibooru(params string[] tags)
|
||||||
|
=> InternalDapiCommand(tags, false, _service.DerpiBooru);
|
||||||
|
|
||||||
|
[NadekoCommand, Aliases]
|
||||||
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
|
public Task Safebooru(params string[] tags)
|
||||||
|
=> InternalDapiCommand(tags, false, _service.SafeBooru);
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
@@ -315,8 +288,11 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
JToken obj;
|
JToken obj;
|
||||||
using (var http = _httpFactory.CreateClient())
|
using (var http = _httpFactory.CreateClient())
|
||||||
{
|
{
|
||||||
obj = JArray.Parse(await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}").ConfigureAwait(false))[0];
|
obj = JArray.Parse(await http
|
||||||
|
.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}")
|
||||||
|
.ConfigureAwait(false))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
|
await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -334,8 +310,11 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
JToken obj;
|
JToken obj;
|
||||||
using (var http = _httpFactory.CreateClient())
|
using (var http = _httpFactory.CreateClient())
|
||||||
{
|
{
|
||||||
obj = JArray.Parse(await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}").ConfigureAwait(false))[0];
|
obj = JArray.Parse(await http
|
||||||
|
.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}")
|
||||||
|
.ConfigureAwait(false))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
|
await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -351,16 +330,16 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(tag))
|
if (string.IsNullOrWhiteSpace(tag))
|
||||||
{
|
{
|
||||||
var blTags = _service.GetBlacklistedTags(ctx.Guild.Id);
|
var blTags = await _service.GetBlacklistedTags(ctx.Guild.Id);
|
||||||
await SendConfirmAsync(GetText(strs.blacklisted_tag_list),
|
await SendConfirmAsync(GetText(strs.blacklisted_tag_list),
|
||||||
blTags.Any()
|
blTags.Any()
|
||||||
? string.Join(", ", blTags)
|
? string.Join(", ", blTags)
|
||||||
: "-").ConfigureAwait(false);
|
: "-").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tag = tag.Trim().ToLowerInvariant();
|
tag = tag.Trim().ToLowerInvariant();
|
||||||
var added = _service.ToggleBlacklistedTag(ctx.Guild.Id, tag);
|
var added = await _service.ToggleBlacklistTag(ctx.Guild.Id, tag);
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
await ReplyPendingLocalizedAsync(strs.blacklisted_tag_add(tag));
|
await ReplyPendingLocalizedAsync(strs.blacklisted_tag_add(tag));
|
||||||
@@ -369,15 +348,6 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
[OwnerOnly]
|
|
||||||
public Task NsfwClearCache()
|
|
||||||
{
|
|
||||||
_service.ClearCache();
|
|
||||||
return ctx.OkAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
@@ -394,12 +364,12 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
|
|
||||||
await SendNhentaiGalleryInternalAsync(g);
|
await SendNhentaiGalleryInternalAsync(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||||
[Priority(0)]
|
[Priority(0)]
|
||||||
public async Task Nhentai([Leftover]string query)
|
public async Task Nhentai([Leftover] string query)
|
||||||
{
|
{
|
||||||
var g = await _service.GetNhentaiBySearchAsync(query);
|
var g = await _service.GetNhentaiBySearchAsync(query);
|
||||||
|
|
||||||
@@ -420,7 +390,7 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
.Select(tag => $"[{tag.Name}]({tag.Url})")
|
.Select(tag => $"[{tag.Name}]({tag.Url})")
|
||||||
.TakeWhile(tag => (count += tag.Length) < 1000)
|
.TakeWhile(tag => (count += tag.Length) < 1000)
|
||||||
.JoinWith(" ");
|
.JoinWith(" ");
|
||||||
|
|
||||||
var embed = _eb.Create()
|
var embed = _eb.Create()
|
||||||
.WithTitle(g.Title)
|
.WithTitle(g.Title)
|
||||||
.WithDescription(g.FullTitle)
|
.WithDescription(g.FullTitle)
|
||||||
@@ -435,27 +405,23 @@ namespace NadekoBot.Modules.NSFW
|
|||||||
await ctx.Channel.EmbedAsync(embed);
|
await ctx.Channel.EmbedAsync(embed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InternalDapiCommand(string tag, DapiSearchType type, bool forceExplicit)
|
private async Task InternalDapiCommand(string[] tags,
|
||||||
|
bool forceExplicit,
|
||||||
|
Func<ulong?, bool, string[], Task<UrlReply>> func)
|
||||||
{
|
{
|
||||||
ImageCacherObject imgObj;
|
var data = await func(ctx.Guild?.Id, forceExplicit, tags);
|
||||||
|
|
||||||
imgObj = await _service.DapiSearch(tag, type, ctx.Guild?.Id, forceExplicit).ConfigureAwait(false);
|
if (data is null || !string.IsNullOrWhiteSpace(data.Error))
|
||||||
|
|
||||||
if (imgObj is null)
|
|
||||||
await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var embed = _eb.Create().WithOkColor()
|
await ReplyErrorLocalizedAsync(strs.no_results);
|
||||||
.WithDescription($"{ctx.User} [{tag ?? "url"}]({imgObj}) ")
|
return;
|
||||||
.WithFooter(type.ToString());
|
|
||||||
|
|
||||||
if (Uri.IsWellFormedUriString(imgObj.FileUrl, UriKind.Absolute))
|
|
||||||
embed.WithImageUrl(imgObj.FileUrl);
|
|
||||||
else
|
|
||||||
Log.Error($"Image link from {type} is not a proper Url: {imgObj.FileUrl}");
|
|
||||||
|
|
||||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
await ctx.Channel.EmbedAsync(_eb
|
||||||
|
.Create(ctx)
|
||||||
|
.WithOkColor()
|
||||||
|
.WithImageUrl(data.Url)
|
||||||
|
.WithDescription($"[link]({data.Url})")
|
||||||
|
.WithFooter($"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
12
src/NadekoBot/Modules/Nsfw/NsfwService.cs
Normal file
12
src/NadekoBot/Modules/Nsfw/NsfwService.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace NadekoBot.Modules.Nsfw
|
||||||
|
{
|
||||||
|
public interface INsfwService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NsfwService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
328
src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs
Normal file
328
src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
using Serilog;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using NadekoBot.Common;
|
||||||
|
using NadekoBot.Extensions;
|
||||||
|
using NadekoBot.Services;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw.Common
|
||||||
|
{
|
||||||
|
public class SearchImageCacher : INService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly Random _rng;
|
||||||
|
|
||||||
|
private static readonly ISet<string> defaultTagBlacklist = new HashSet<string>()
|
||||||
|
{
|
||||||
|
"loli",
|
||||||
|
"lolicon",
|
||||||
|
"shota",
|
||||||
|
"shotacon",
|
||||||
|
"cub"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly Dictionary<Booru, object> _typeLocks = new();
|
||||||
|
private readonly Dictionary<Booru, HashSet<string>> _usedTags = new();
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
public SearchImageCacher(IHttpClientFactory httpFactory, IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
_rng = new NadekoRandom();
|
||||||
|
_cache = cache;
|
||||||
|
|
||||||
|
// initialize new cache with empty values
|
||||||
|
foreach (var type in Enum.GetValues<Booru>())
|
||||||
|
{
|
||||||
|
_typeLocks[type] = new();
|
||||||
|
_usedTags[type] = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Key(Booru boory, string tag)
|
||||||
|
=> $"booru:{boory}__tag:{tag}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download images of the specified type, and cache them.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tags">Required tags</param>
|
||||||
|
/// <param name="forceExplicit">Whether images will be forced to be explicit</param>
|
||||||
|
/// <param name="type">Provider type</param>
|
||||||
|
/// <param name="cancel">Cancellation token</param>
|
||||||
|
/// <returns>Whether any image is found.</returns>
|
||||||
|
private async Task<bool> UpdateImagesInternalAsync(string[] tags, bool forceExplicit, Booru type, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var images = await DownloadImagesAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false);
|
||||||
|
if (images is null || images.Count == 0)
|
||||||
|
{
|
||||||
|
// Log.Warning("Got no images for {0}, tags: {1}", type, string.Join(", ", tags));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("Updating {0}...", type);
|
||||||
|
lock (_typeLocks[type])
|
||||||
|
{
|
||||||
|
var typeUsedTags = _usedTags[type];
|
||||||
|
foreach (var tag in tags)
|
||||||
|
typeUsedTags.Add(tag);
|
||||||
|
|
||||||
|
// if user uses no tags for the hentai command and there are no used
|
||||||
|
// tags atm, just select 50 random tags from downloaded images to seed
|
||||||
|
if (typeUsedTags.Count == 0)
|
||||||
|
images.SelectMany(x => x.Tags)
|
||||||
|
.Distinct()
|
||||||
|
.Shuffle()
|
||||||
|
.Take(50)
|
||||||
|
.ForEach(x => typeUsedTags.Add(x));
|
||||||
|
|
||||||
|
foreach (var img in images)
|
||||||
|
{
|
||||||
|
// if any of the tags is a tag banned by discord
|
||||||
|
// do not put that image in the cache
|
||||||
|
if (defaultTagBlacklist.Overlaps(img.Tags))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// if image doesn't have a proper absolute uri, skip it
|
||||||
|
if (!Uri.IsWellFormedUriString(img.FileUrl, UriKind.Absolute))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// i'm appending current tags because of tag aliasing
|
||||||
|
// this way, if user uses tag alias, for example 'kissing' -
|
||||||
|
// both 'kiss' (real tag returned by the image) and 'kissing' will be populated with
|
||||||
|
// retreived images
|
||||||
|
foreach (var tag in img.Tags.Concat(tags).Distinct())
|
||||||
|
{
|
||||||
|
if (typeUsedTags.Contains(tag))
|
||||||
|
{
|
||||||
|
var set = _cache.GetOrCreate<HashSet<ImageData>>(Key(type, tag), e =>
|
||||||
|
{
|
||||||
|
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
|
||||||
|
return new();
|
||||||
|
});
|
||||||
|
|
||||||
|
if(set.Count < 100)
|
||||||
|
set.Add(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageData QueryLocal(string[] tags, bool forceExplicit, Booru type, HashSet<string> blacklistedTags)
|
||||||
|
{
|
||||||
|
var setList = new List<HashSet<ImageData>>();
|
||||||
|
|
||||||
|
// ofc make sure no changes are happening while we're getting a random one
|
||||||
|
lock (_typeLocks[type])
|
||||||
|
{
|
||||||
|
// if no tags are provided, get a random tag
|
||||||
|
if (tags.Length == 0)
|
||||||
|
{
|
||||||
|
// get all tags in the cache
|
||||||
|
if (_usedTags.TryGetValue(type, out var allTags)
|
||||||
|
&& allTags.Count > 0)
|
||||||
|
{
|
||||||
|
tags = new[] {allTags.ToList()[_rng.Next(0, allTags.Count)]};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
// if any tag is missing from cache, that means there is no result
|
||||||
|
if (_cache.TryGetValue<HashSet<ImageData>>(Key(type, tag), out var set))
|
||||||
|
setList.Add(set);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setList.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
|
||||||
|
List<ImageData> resultList;
|
||||||
|
// if multiple tags, we need to interesect sets
|
||||||
|
if (setList.Count > 1)
|
||||||
|
{
|
||||||
|
// now that we have sets, interesect them to find eligible items
|
||||||
|
// make a copy of the 1st set
|
||||||
|
var resultSet = new HashSet<ImageData>(setList[0]);
|
||||||
|
|
||||||
|
// go through all other sets, and
|
||||||
|
for (var i = 1; i < setList.Count; ++i)
|
||||||
|
{
|
||||||
|
// if any of the elements in result set are not present in the current set
|
||||||
|
// remove it from the result set
|
||||||
|
resultSet.IntersectWith(setList[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultList = resultSet.ToList();
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if only one tag, use that set
|
||||||
|
resultList = setList[0].ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a random one which doesn't have blacklisted tags in it
|
||||||
|
resultList = resultList.Where(x => !blacklistedTags.Overlaps(x.Tags)).ToList();
|
||||||
|
|
||||||
|
// if no items in the set -> not found
|
||||||
|
if (resultList.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var toReturn = resultList[_rng.Next(0, resultList.Count)];
|
||||||
|
|
||||||
|
// remove from cache
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue<HashSet<ImageData>>(Key(type, tag), out var items))
|
||||||
|
{
|
||||||
|
items.Remove(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImageData> GetImageNew(string[] tags, bool forceExplicit, Booru type,
|
||||||
|
HashSet<string> blacklistedTags, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
// make sure tags are proper
|
||||||
|
tags = tags
|
||||||
|
.Where(x => x is not null)
|
||||||
|
.Select(tag => tag.ToLowerInvariant().Trim())
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (tags.Length > 2 && type == Booru.Danbooru)
|
||||||
|
tags = tags[..2];
|
||||||
|
|
||||||
|
// use both tags banned by discord and tags banned on the server
|
||||||
|
if (blacklistedTags.Overlaps(tags) || defaultTagBlacklist.Overlaps(tags))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
// query for an image
|
||||||
|
var image = QueryLocal(tags, forceExplicit, type, blacklistedTags);
|
||||||
|
if (image is not null)
|
||||||
|
return image;
|
||||||
|
|
||||||
|
var success = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// if image is not found, update the cache and query again
|
||||||
|
success = await UpdateImagesInternalAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
return default;
|
||||||
|
image = QueryLocal(tags, forceExplicit, type, blacklistedTags);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<(Booru, string), int> maxPages = new();
|
||||||
|
|
||||||
|
public async Task<List<ImageData>> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var tagStr = string.Join(' ', tags.OrderByDescending(x => x));
|
||||||
|
var page = 0;
|
||||||
|
|
||||||
|
var attempt = 0;
|
||||||
|
while (attempt++ <= 10)
|
||||||
|
{
|
||||||
|
if (maxPages.TryGetValue((type, tagStr), out var maxPage))
|
||||||
|
{
|
||||||
|
if (maxPage == 0)
|
||||||
|
{
|
||||||
|
Log.Information("Tag {0} yields no result on {1}, skipping.", tagStr, type);
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
page = _rng.Next(0, maxPage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
page = _rng.Next(0, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await DownloadImagesAsync(tags, isExplicit, type, page, cancel).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (result is null or { Count: 0 })
|
||||||
|
{
|
||||||
|
Log.Information("Tag {0}, page {1} has no result on {2}.", string.Join(", ", tags), page, type.ToString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IImageDownloader GetImageDownloader(Booru booru, HttpClient http)
|
||||||
|
=> booru switch
|
||||||
|
{
|
||||||
|
Booru.Danbooru => new DanbooruImageDownloader(http),
|
||||||
|
Booru.Yandere => new YandereImageDownloader(http),
|
||||||
|
Booru.Konachan => new KonachanImageDownloader(http),
|
||||||
|
Booru.Safebooru => new SafebooruImageDownloader(http),
|
||||||
|
Booru.E621 => new E621ImageDownloader(http),
|
||||||
|
Booru.Derpibooru => new DerpibooruImageDownloader(http),
|
||||||
|
Booru.Gelbooru => new GelbooruImageDownloader(http),
|
||||||
|
Booru.Rule34 => new Rule34ImageDownloader(http),
|
||||||
|
Booru.Sankaku => new SankakuImageDownloader(http),
|
||||||
|
_ => throw new NotImplementedException($"{booru} downloader not implemented.")
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<List<ImageData>> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, int page, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("Downloading from {0} (page {1})...", type, page);
|
||||||
|
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
var downloader = GetImageDownloader(type, http);
|
||||||
|
|
||||||
|
var images = await downloader.DownloadImageDataAsync(tags, page, isExplicit, cancel);
|
||||||
|
if (images.Count == 0)
|
||||||
|
{
|
||||||
|
var tagStr = string.Join(' ', tags.OrderByDescending(x => x));
|
||||||
|
maxPages[(type, tagStr)] = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error downloading an image:\nTags: {0}\nType: {1}\nPage: {2}\nMessage: {3}",
|
||||||
|
string.Join(", ", tags),
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
ex.Message);
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
393
src/NadekoBot/Modules/Nsfw/SearchImagesService.cs
Normal file
393
src/NadekoBot/Modules/Nsfw/SearchImagesService.cs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Serilog;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using LinqToDB;
|
||||||
|
using NadekoBot.Common;
|
||||||
|
using NadekoBot.Extensions;
|
||||||
|
using NadekoBot.Modules.Searches.Common;
|
||||||
|
using NadekoBot.Services;
|
||||||
|
using NadekoBot.Services.Database.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Booru = NadekoBot.Modules.Nsfw.Common.Booru;
|
||||||
|
using SearchImageCacher = NadekoBot.Modules.Nsfw.Common.SearchImageCacher;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw
|
||||||
|
{
|
||||||
|
public record TagRequest(ulong GuildId, bool ForceExplicit, Booru SearchType, params string[] Tags);
|
||||||
|
public record UrlReply
|
||||||
|
{
|
||||||
|
public string Error { get; init; }
|
||||||
|
public string Url { get; init; }
|
||||||
|
public string Rating { get; init; }
|
||||||
|
public string Provider { get; init; }
|
||||||
|
public List<string> Tags { get; } = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SearchImagesService : ISearchImagesService, INService
|
||||||
|
{
|
||||||
|
private readonly Random _rng;
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly SearchImageCacher _cache;
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly DbService _db;
|
||||||
|
private ConcurrentDictionary<ulong, HashSet<string>> BlacklistedTags { get; }
|
||||||
|
|
||||||
|
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
||||||
|
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
||||||
|
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
||||||
|
|
||||||
|
public SearchImagesService(DbService db,
|
||||||
|
IHttpClientFactory http,
|
||||||
|
SearchImageCacher cacher,
|
||||||
|
IHttpClientFactory httpFactory)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_rng = new NadekoRandom();
|
||||||
|
_http = http.CreateClient();
|
||||||
|
_http.AddFakeHeaders();
|
||||||
|
_cache = cacher;
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
|
||||||
|
using var uow = db.GetDbContext();
|
||||||
|
BlacklistedTags = new(
|
||||||
|
uow.NsfwBlacklistedTags
|
||||||
|
.AsEnumerable()
|
||||||
|
.GroupBy(x => x.GuildId)
|
||||||
|
.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => new HashSet<string>(x.Select(x => x.Tag))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<UrlReply> GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty<string>(), forceExplicit, dapi, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or /
|
||||||
|
|
||||||
|
private async Task<UrlReply> GetNsfwImageAsync(
|
||||||
|
ulong guildId,
|
||||||
|
string[] tags,
|
||||||
|
bool forceExplicit,
|
||||||
|
Booru dapi,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (!tags.All(x => IsValidTag(x)))
|
||||||
|
{
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = "One or more tags are invalid.",
|
||||||
|
Url = ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BlacklistedTags.TryGetValue(guildId, out var blTags);
|
||||||
|
|
||||||
|
if (dapi == Booru.E621) {
|
||||||
|
for (var i = 0; i < tags.Length; ++i)
|
||||||
|
if (tags[i] == "yuri")
|
||||||
|
tags[i] = "female/female";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dapi == Booru.Derpibooru)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < tags.Length; ++i)
|
||||||
|
if (tags[i] == "yuri")
|
||||||
|
tags[i] = "lesbian";
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _cache.GetImageNew(tags, forceExplicit, dapi, blTags ?? new HashSet<string>(), cancel)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = "Image not found.",
|
||||||
|
Url = ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply = new UrlReply
|
||||||
|
{
|
||||||
|
Error = "",
|
||||||
|
Url = result.FileUrl,
|
||||||
|
Rating = result.Rating,
|
||||||
|
Provider = result.SearchType.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
reply.Tags.AddRange(result.Tags);
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Failed getting {Dapi} image: {Message}", dapi, ex.Message);
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = ex.Message,
|
||||||
|
Url = ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<UrlReply> Gelbooru(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru);
|
||||||
|
|
||||||
|
public Task<UrlReply> Danbooru(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Danbooru);
|
||||||
|
|
||||||
|
public Task<UrlReply> Konachan(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Konachan);
|
||||||
|
|
||||||
|
public Task<UrlReply> Yandere(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Yandere);
|
||||||
|
|
||||||
|
public Task<UrlReply> Rule34(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Rule34);
|
||||||
|
|
||||||
|
public Task<UrlReply> E621(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.E621);
|
||||||
|
|
||||||
|
public Task<UrlReply> DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Derpibooru);
|
||||||
|
|
||||||
|
public Task<UrlReply> SafeBooru(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Safebooru);
|
||||||
|
|
||||||
|
public Task<UrlReply> Sankaku(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
=> GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Sankaku);
|
||||||
|
|
||||||
|
public async Task<UrlReply> Hentai(ulong? guildId, bool forceExplicit, string[] tags)
|
||||||
|
{
|
||||||
|
var providers = new[] {
|
||||||
|
Booru.Danbooru,
|
||||||
|
Booru.Konachan,
|
||||||
|
Booru.Gelbooru,
|
||||||
|
Booru.Yandere
|
||||||
|
};
|
||||||
|
|
||||||
|
using var cancelSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// create a task for each type
|
||||||
|
var tasks = providers.Select(type => GetNsfwImageAsync(guildId, forceExplicit, tags, type)).ToList();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// wait for any of the tasks to complete
|
||||||
|
var task = await Task.WhenAny(tasks);
|
||||||
|
|
||||||
|
// get its result
|
||||||
|
var result = task.GetAwaiter().GetResult();
|
||||||
|
if(result.Error == "")
|
||||||
|
{
|
||||||
|
// if we have a non-error result, cancel other searches and return the result
|
||||||
|
cancelSource.Cancel();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the result is an error, remove that task from the waiting list,
|
||||||
|
// and wait for another task to complete
|
||||||
|
tasks.Remove(task);
|
||||||
|
}
|
||||||
|
while (tasks.Count > 0); // keep looping as long as there is any task remaining to be attempted
|
||||||
|
|
||||||
|
// if we ran out of tasks, that means all tasks failed - return an error
|
||||||
|
return new UrlReply()
|
||||||
|
{
|
||||||
|
Error = "No hentai image found."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UrlReply> Boobs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JToken obj;
|
||||||
|
obj = JArray.Parse(await _http.GetStringAsync($"http://api.oboobs.ru/boobs/{_rng.Next(0, 12000)}").ConfigureAwait(false))[0];
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = "",
|
||||||
|
Url = $"http://media.oboobs.ru/{obj["preview"]}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message);
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = ex.Message,
|
||||||
|
Url = "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object taglock = new object();
|
||||||
|
public ValueTask<bool> ToggleBlacklistTag(ulong guildId, string tag)
|
||||||
|
{
|
||||||
|
lock (taglock)
|
||||||
|
{
|
||||||
|
tag = tag.Trim().ToLowerInvariant();
|
||||||
|
var blacklistedTags = BlacklistedTags.GetOrAdd(guildId, new HashSet<string>());
|
||||||
|
var isAdded = blacklistedTags.Add(tag);
|
||||||
|
|
||||||
|
using var uow = _db.GetDbContext();
|
||||||
|
if (!isAdded)
|
||||||
|
{
|
||||||
|
blacklistedTags.Remove(tag);
|
||||||
|
uow.NsfwBlacklistedTags.DeleteAsync(x => x.GuildId == guildId && x.Tag == tag);
|
||||||
|
uow.SaveChanges();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uow.NsfwBlacklistedTags.Add(new NsfwBlacklistedTag()
|
||||||
|
{
|
||||||
|
Tag = tag,
|
||||||
|
GuildId = guildId
|
||||||
|
});
|
||||||
|
|
||||||
|
uow.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(isAdded);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<string[]> GetBlacklistedTags(ulong guildId)
|
||||||
|
{
|
||||||
|
lock (taglock)
|
||||||
|
{
|
||||||
|
if (BlacklistedTags.TryGetValue(guildId, out var tags))
|
||||||
|
{
|
||||||
|
return new(tags.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(Array.Empty<string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UrlReply> Butts()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JToken obj;
|
||||||
|
obj = JArray.Parse(await _http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0];
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = "",
|
||||||
|
Url = $"http://media.obutts.ru/{obj["preview"]}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error retreiving butt image: {Message}", ex.Message);
|
||||||
|
return new UrlReply
|
||||||
|
{
|
||||||
|
Error = ex.Message,
|
||||||
|
Url = "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Nhentai
|
||||||
|
private string GetNhentaiExtensionInternal(string s)
|
||||||
|
=> s switch
|
||||||
|
{
|
||||||
|
"j" => "jpg",
|
||||||
|
"p" => "png",
|
||||||
|
"g" => "gif",
|
||||||
|
_ => "jpg"
|
||||||
|
};
|
||||||
|
|
||||||
|
private Gallery ModelToGallery(NhentaiApiModel.Gallery model)
|
||||||
|
{
|
||||||
|
var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb."
|
||||||
|
+ GetNhentaiExtensionInternal(model.Images.Thumbnail.T);
|
||||||
|
|
||||||
|
var url = $"https://nhentai.net/g/{model.Id}";
|
||||||
|
return new Gallery(
|
||||||
|
model.Id.ToString(),
|
||||||
|
url,
|
||||||
|
model.Title.English,
|
||||||
|
model.Title.Pretty,
|
||||||
|
thumbnail,
|
||||||
|
model.NumPages,
|
||||||
|
model.NumFavorites,
|
||||||
|
model.UploadDate.ToUnixTimestamp().UtcDateTime,
|
||||||
|
model.Tags.Map(x => new Tag()
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
Url = "https://nhentai.com/" + x.Url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<NhentaiApiModel.Gallery> GetNhentaiByIdInternalAsync(uint id)
|
||||||
|
{
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id);
|
||||||
|
return JsonConvert.DeserializeObject<NhentaiApiModel.Gallery>(res);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
Log.Warning("Nhentai with id {NhentaiId} not found", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<NhentaiApiModel.Gallery[]> SearchNhentaiInternalAsync(string search)
|
||||||
|
{
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search);
|
||||||
|
return JsonConvert.DeserializeObject<NhentaiApiModel.SearchResult>(res).Result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
Log.Warning("Nhentai with search {NhentaiSearch} not found", search);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Gallery> GetNhentaiByIdAsync(uint id)
|
||||||
|
{
|
||||||
|
var model = await GetNhentaiByIdInternalAsync(id);
|
||||||
|
|
||||||
|
return ModelToGallery(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] _bannedTags =
|
||||||
|
{
|
||||||
|
"loli",
|
||||||
|
"lolicon",
|
||||||
|
"shota",
|
||||||
|
"shotacon",
|
||||||
|
"cub"
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task<Gallery> GetNhentaiBySearchAsync(string search)
|
||||||
|
{
|
||||||
|
var models = await SearchNhentaiInternalAsync(search);
|
||||||
|
|
||||||
|
models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray();
|
||||||
|
|
||||||
|
if (models.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ModelToGallery(models[_rng.Next(0, models.Length)]);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@@ -1,18 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using NadekoBot.Modules.Nsfw.Common;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common
|
namespace NadekoBot.Modules.Searches.Common
|
||||||
{
|
{
|
||||||
public class ImageCacherObject : IComparable<ImageCacherObject>
|
public class ImageCacherObject : IComparable<ImageCacherObject>
|
||||||
{
|
{
|
||||||
public DapiSearchType SearchType { get; }
|
public Booru SearchType { get; }
|
||||||
public string FileUrl { get; }
|
public string FileUrl { get; }
|
||||||
public HashSet<string> Tags { get; }
|
public HashSet<string> Tags { get; }
|
||||||
public string Rating { get; }
|
public string Rating { get; }
|
||||||
|
|
||||||
public ImageCacherObject(DapiImageObject obj, DapiSearchType type)
|
public ImageCacherObject(DapiImageObject obj, Booru type)
|
||||||
{
|
{
|
||||||
if (type == DapiSearchType.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
|
if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
|
||||||
{
|
{
|
||||||
this.FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
|
this.FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
|
||||||
}
|
}
|
||||||
@@ -25,7 +26,7 @@ namespace NadekoBot.Modules.Searches.Common
|
|||||||
this.Tags = new HashSet<string>((obj.Tags ?? obj.TagString).Split(' '));
|
this.Tags = new HashSet<string>((obj.Tags ?? obj.TagString).Split(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageCacherObject(string url, DapiSearchType type, string tags, string rating)
|
public ImageCacherObject(string url, Booru type, string tags, string rating)
|
||||||
{
|
{
|
||||||
this.SearchType = type;
|
this.SearchType = type;
|
||||||
this.FileUrl = url;
|
this.FileUrl = url;
|
||||||
|
@@ -1,312 +0,0 @@
|
|||||||
using NadekoBot.Extensions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Xml;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common
|
|
||||||
{
|
|
||||||
// note: this is not the code that public nadeko is using
|
|
||||||
public class SearchImageCacher
|
|
||||||
{
|
|
||||||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
private readonly Random _rng;
|
|
||||||
private readonly SortedSet<ImageCacherObject> _cache;
|
|
||||||
private static readonly List<string> defaultTagBlacklist = new List<string>() {
|
|
||||||
"loli",
|
|
||||||
"lolicon",
|
|
||||||
"shota"
|
|
||||||
};
|
|
||||||
|
|
||||||
public SearchImageCacher(IHttpClientFactory http)
|
|
||||||
{
|
|
||||||
_httpFactory = http;
|
|
||||||
_rng = new Random();
|
|
||||||
_cache = new SortedSet<ImageCacherObject>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ImageCacherObject> GetImage(string[] tags, bool forceExplicit, DapiSearchType type,
|
|
||||||
HashSet<string> blacklistedTags = null)
|
|
||||||
{
|
|
||||||
tags = tags.Select(tag => tag?.ToLowerInvariant()).ToArray();
|
|
||||||
|
|
||||||
blacklistedTags = blacklistedTags ?? new HashSet<string>();
|
|
||||||
|
|
||||||
foreach (var item in defaultTagBlacklist)
|
|
||||||
{
|
|
||||||
blacklistedTags.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
blacklistedTags = blacklistedTags.Select(t => t.ToLowerInvariant()).ToHashSet();
|
|
||||||
|
|
||||||
if (tags.Any(x => blacklistedTags.Contains(x)))
|
|
||||||
{
|
|
||||||
throw new Exception("One of the specified tags is blacklisted");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == DapiSearchType.E621)
|
|
||||||
tags = tags.Select(tag => tag?.Replace("yuri", "female/female", StringComparison.InvariantCulture))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
await _lock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ImageCacherObject[] imgs;
|
|
||||||
if (tags.Any())
|
|
||||||
{
|
|
||||||
imgs = _cache.Where(x => x.Tags.IsSupersetOf(tags) && x.SearchType == type && (!forceExplicit || x.Rating == "e")).ToArray();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
imgs = _cache.Where(x => x.SearchType == type).ToArray();
|
|
||||||
}
|
|
||||||
imgs = imgs.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant()))).ToArray();
|
|
||||||
ImageCacherObject img;
|
|
||||||
if (imgs.Length == 0)
|
|
||||||
img = null;
|
|
||||||
else
|
|
||||||
img = imgs[_rng.Next(imgs.Length)];
|
|
||||||
|
|
||||||
if (img != null)
|
|
||||||
{
|
|
||||||
_cache.Remove(img);
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var images = await DownloadImagesAsync(tags, forceExplicit, type).ConfigureAwait(false);
|
|
||||||
images = images
|
|
||||||
.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant())))
|
|
||||||
.ToArray();
|
|
||||||
if (images.Length == 0)
|
|
||||||
return null;
|
|
||||||
var toReturn = images[_rng.Next(images.Length)];
|
|
||||||
foreach (var dledImg in images)
|
|
||||||
{
|
|
||||||
if (dledImg != toReturn)
|
|
||||||
_cache.Add(dledImg);
|
|
||||||
}
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ImageCacherObject[]> DownloadImagesAsync(string[] tags, bool isExplicit, DapiSearchType type)
|
|
||||||
{
|
|
||||||
isExplicit = type == DapiSearchType.Safebooru
|
|
||||||
? false
|
|
||||||
: isExplicit;
|
|
||||||
var tag = "";
|
|
||||||
tag += string.Join('+', tags.Select(x => x.Replace(" ", "_", StringComparison.InvariantCulture).ToLowerInvariant()));
|
|
||||||
if (isExplicit)
|
|
||||||
tag = "rating%3Aexplicit+" + tag;
|
|
||||||
var website = "";
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case DapiSearchType.Safebooru:
|
|
||||||
website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}&json=1";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.E621:
|
|
||||||
website = $"https://e621.net/posts.json?limit=200&tags={tag}";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Danbooru:
|
|
||||||
website = $"http://danbooru.donmai.us/posts.json?limit=100&tags={tag}";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Gelbooru:
|
|
||||||
website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Rule34:
|
|
||||||
website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Konachan:
|
|
||||||
website = $"https://konachan.com/post.json?s=post&q=index&limit=100&tags={tag}";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Yandere:
|
|
||||||
website = $"https://yande.re/post.json?limit=100&tags={tag}";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Derpibooru:
|
|
||||||
tag = string.IsNullOrWhiteSpace(tag) ? "safe" : tag;
|
|
||||||
website = $"https://www.derpibooru.org/api/v1/json/search/images?q={tag?.Replace('+', ',')}&per_page=49";
|
|
||||||
break;
|
|
||||||
case DapiSearchType.Sankaku:
|
|
||||||
website = $"https://capi-v2.sankakucomplex.com/posts?tags={tag}&limit=50";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var _http = _httpFactory.CreateClient())
|
|
||||||
{
|
|
||||||
_http.AddFakeHeaders();
|
|
||||||
if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || type == DapiSearchType.Danbooru)
|
|
||||||
{
|
|
||||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
|
||||||
return JsonConvert.DeserializeObject<DapiImageObject[]>(data)
|
|
||||||
.Where(x => x.FileUrl != null)
|
|
||||||
.Select(x => new ImageCacherObject(x, type))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == DapiSearchType.Sankaku)
|
|
||||||
{
|
|
||||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
|
||||||
return JsonConvert.DeserializeObject<SankakuImageObject[]>(data)
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
|
|
||||||
.Select(x => new ImageCacherObject(
|
|
||||||
x.FileUrl,
|
|
||||||
DapiSearchType.Sankaku,
|
|
||||||
x.Tags.Select(x => x.Name).JoinWith(','),
|
|
||||||
x.Score))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == DapiSearchType.E621)
|
|
||||||
{
|
|
||||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
|
||||||
return JsonConvert.DeserializeAnonymousType(data, new { posts = new List<E621Object>() })
|
|
||||||
.posts
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url))
|
|
||||||
.Select(x => new ImageCacherObject(x.File.Url,
|
|
||||||
type, string.Join(' ', x.Tags.General), x.Score.Total))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == DapiSearchType.Derpibooru)
|
|
||||||
{
|
|
||||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
|
||||||
return JsonConvert.DeserializeObject<DerpiContainer>(data)
|
|
||||||
.Images
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl))
|
|
||||||
.Select(x => new ImageCacherObject(x.ViewUrl,
|
|
||||||
type, string.Join("\n", x.Tags), x.Score))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == DapiSearchType.Safebooru)
|
|
||||||
{
|
|
||||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
|
||||||
return JsonConvert.DeserializeObject<SafebooruElement[]>(data)
|
|
||||||
.Select(x => new ImageCacherObject(x.FileUrl, type, x.Tags, x.Rating))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await LoadXmlAsync(website, type).ConfigureAwait(false)).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "Error downloading an image: {Message}", ex.Message);
|
|
||||||
return Array.Empty<ImageCacherObject>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ImageCacherObject[]> LoadXmlAsync(string website, DapiSearchType type)
|
|
||||||
{
|
|
||||||
var list = new List<ImageCacherObject>();
|
|
||||||
using (var http = _httpFactory.CreateClient())
|
|
||||||
using (var stream = await http.GetStreamAsync(website).ConfigureAwait(false))
|
|
||||||
using (var reader = XmlReader.Create(stream, new XmlReaderSettings()
|
|
||||||
{
|
|
||||||
Async = true,
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
while (await reader.ReadAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
if (reader.NodeType == XmlNodeType.Element &&
|
|
||||||
reader.Name == "post")
|
|
||||||
{
|
|
||||||
list.Add(new ImageCacherObject(new DapiImageObject()
|
|
||||||
{
|
|
||||||
FileUrl = reader["file_url"],
|
|
||||||
Tags = reader["tags"],
|
|
||||||
Rating = reader["rating"] ?? "e"
|
|
||||||
|
|
||||||
}, type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_cache.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class DapiImageObject
|
|
||||||
{
|
|
||||||
[JsonProperty("File_Url")]
|
|
||||||
public string FileUrl { get; set; }
|
|
||||||
public string Tags { get; set; }
|
|
||||||
[JsonProperty("Tag_String")]
|
|
||||||
public string TagString { get; set; }
|
|
||||||
public string Rating { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DerpiContainer
|
|
||||||
{
|
|
||||||
public DerpiImageObject[] Images { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DerpiImageObject
|
|
||||||
{
|
|
||||||
[JsonProperty("view_url")]
|
|
||||||
public string ViewUrl { get; set; }
|
|
||||||
public string[] Tags { get; set; }
|
|
||||||
public string Score { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SankakuImageObject
|
|
||||||
{
|
|
||||||
public class Tag
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonProperty("file_url")]
|
|
||||||
public string FileUrl { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("file_type")]
|
|
||||||
public string FileType { get; set; }
|
|
||||||
|
|
||||||
public Tag[] Tags { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("total_score")]
|
|
||||||
public string Score { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DapiSearchType
|
|
||||||
{
|
|
||||||
Safebooru,
|
|
||||||
E621,
|
|
||||||
Derpibooru,
|
|
||||||
Gelbooru,
|
|
||||||
Konachan,
|
|
||||||
Rule34,
|
|
||||||
Yandere,
|
|
||||||
Danbooru,
|
|
||||||
Sankaku,
|
|
||||||
}
|
|
||||||
public class SafebooruElement
|
|
||||||
{
|
|
||||||
public string Directory { get; set; }
|
|
||||||
public string Image { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}";
|
|
||||||
public string Rating { get; set; }
|
|
||||||
public string Tags { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -22,6 +22,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NadekoBot.Modules.Administration.Services;
|
using NadekoBot.Modules.Administration.Services;
|
||||||
|
using NadekoBot.Modules.Nsfw.Common;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Configuration = AngleSharp.Configuration;
|
using Configuration = AngleSharp.Configuration;
|
||||||
|
|
||||||
@@ -589,10 +590,6 @@ namespace NadekoBot.Modules.Searches
|
|||||||
await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={imageLink}").ConfigureAwait(false);
|
await SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={imageLink}").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
|
||||||
public Task Safebooru([Leftover] string tag = null)
|
|
||||||
=> InternalDapiCommand(tag, DapiSearchType.Safebooru);
|
|
||||||
|
|
||||||
[NadekoCommand, Aliases]
|
[NadekoCommand, Aliases]
|
||||||
public async Task Wiki([Leftover] string query = null)
|
public async Task Wiki([Leftover] string query = null)
|
||||||
{
|
{
|
||||||
@@ -760,21 +757,6 @@ namespace NadekoBot.Modules.Searches
|
|||||||
await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}").ConfigureAwait(false);
|
await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InternalDapiCommand(string tag, DapiSearchType type)
|
|
||||||
{
|
|
||||||
tag = tag?.Trim() ?? "";
|
|
||||||
|
|
||||||
var imgObj = await _service.DapiSearch(tag, type, ctx.Guild?.Id).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (imgObj is null)
|
|
||||||
await SendErrorAsync(ctx.User.Mention + " " + GetText(strs.no_results)).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
|
||||||
.WithDescription($"{ctx.User.Mention} [{tag ?? "url"}]({imgObj.FileUrl})")
|
|
||||||
.WithImageUrl(imgObj.FileUrl)
|
|
||||||
.WithFooter(type.ToString())).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateQuery(IMessageChannel ch, string query)
|
public async Task<bool> ValidateQuery(IMessageChannel ch, string query)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(query))
|
if (!string.IsNullOrWhiteSpace(query))
|
||||||
|
@@ -50,14 +50,6 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
|
|
||||||
public List<WoWJoke> WowJokes { get; } = new List<WoWJoke>();
|
public List<WoWJoke> WowJokes { get; } = new List<WoWJoke>();
|
||||||
public List<MagicItem> MagicItems { get; } = new List<MagicItem>();
|
public List<MagicItem> MagicItems { get; } = new List<MagicItem>();
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, SearchImageCacher> _imageCacher = new ConcurrentDictionary<ulong, SearchImageCacher>();
|
|
||||||
|
|
||||||
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
|
||||||
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
|
||||||
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, HashSet<string>> _blacklistedTags = new ConcurrentDictionary<ulong, HashSet<string>>();
|
|
||||||
private readonly List<string> _yomamaJokes;
|
private readonly List<string> _yomamaJokes;
|
||||||
|
|
||||||
public SearchesService(DiscordSocketClient client, IGoogleApiService google,
|
public SearchesService(DiscordSocketClient client, IGoogleApiService google,
|
||||||
@@ -75,11 +67,6 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
_eb = eb;
|
_eb = eb;
|
||||||
_rng = new NadekoRandom();
|
_rng = new NadekoRandom();
|
||||||
|
|
||||||
_blacklistedTags = new ConcurrentDictionary<ulong, HashSet<string>>(
|
|
||||||
bot.AllGuildConfigs.ToDictionary(
|
|
||||||
x => x.GuildId,
|
|
||||||
x => new HashSet<string>(x.NsfwBlacklistedTags.Select(y => y.Tag))));
|
|
||||||
|
|
||||||
//translate commands
|
//translate commands
|
||||||
_client.MessageReceived += (msg) =>
|
_client.MessageReceived += (msg) =>
|
||||||
{
|
{
|
||||||
@@ -366,80 +353,6 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(true);
|
return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ImageCacherObject> DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false)
|
|
||||||
{
|
|
||||||
tag = tag ?? "";
|
|
||||||
if (string.IsNullOrWhiteSpace(tag)
|
|
||||||
&& (tag.Contains("loli") || tag.Contains("shota")))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags = tag
|
|
||||||
.Split('+')
|
|
||||||
.Select(x => x.ToLowerInvariant().Replace(' ', '_'))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (guild.HasValue)
|
|
||||||
{
|
|
||||||
var blacklistedTags = GetBlacklistedTags(guild.Value);
|
|
||||||
|
|
||||||
var cacher = _imageCacher.GetOrAdd(guild.Value, (key) => new SearchImageCacher(_httpFactory));
|
|
||||||
|
|
||||||
return cacher.GetImage(tags, isExplicit, type, blacklistedTags);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher(_httpFactory));
|
|
||||||
|
|
||||||
return cacher.GetImage(tags, isExplicit, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public HashSet<string> GetBlacklistedTags(ulong guildId)
|
|
||||||
{
|
|
||||||
if (_blacklistedTags.TryGetValue(guildId, out var tags))
|
|
||||||
return tags;
|
|
||||||
return new HashSet<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ToggleBlacklistedTag(ulong guildId, string tag)
|
|
||||||
{
|
|
||||||
var tagObj = new NsfwBlacklitedTag
|
|
||||||
{
|
|
||||||
Tag = tag
|
|
||||||
};
|
|
||||||
|
|
||||||
bool added;
|
|
||||||
using (var uow = _db.GetDbContext())
|
|
||||||
{
|
|
||||||
var gc = uow.GuildConfigsForId(guildId, set => set.Include(y => y.NsfwBlacklistedTags));
|
|
||||||
if (gc.NsfwBlacklistedTags.Add(tagObj))
|
|
||||||
added = true;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
gc.NsfwBlacklistedTags.Remove(tagObj);
|
|
||||||
var toRemove = gc.NsfwBlacklistedTags.FirstOrDefault(x => x.Equals(tagObj));
|
|
||||||
if (toRemove != null)
|
|
||||||
uow.Remove(toRemove);
|
|
||||||
added = false;
|
|
||||||
}
|
|
||||||
var newTags = new HashSet<string>(gc.NsfwBlacklistedTags.Select(x => x.Tag));
|
|
||||||
_blacklistedTags.AddOrUpdate(guildId, newTags, delegate { return newTags; });
|
|
||||||
|
|
||||||
uow.SaveChanges();
|
|
||||||
}
|
|
||||||
return added;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearCache()
|
|
||||||
{
|
|
||||||
foreach (var c in _imageCacher)
|
|
||||||
{
|
|
||||||
c.Value?.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly object yomamaLock = new object();
|
private readonly object yomamaLock = new object();
|
||||||
private int yomamaJokeIndex = 0;
|
private int yomamaJokeIndex = 0;
|
||||||
public Task<string> GetYomamaJoke()
|
public Task<string> GetYomamaJoke()
|
||||||
@@ -838,95 +751,5 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
fullQueryLink,
|
fullQueryLink,
|
||||||
"0");
|
"0");
|
||||||
}
|
}
|
||||||
#region Nhentai
|
|
||||||
private string GetNhentaiExtensionInternal(string s)
|
|
||||||
=> s switch
|
|
||||||
{
|
|
||||||
"j" => "jpg",
|
|
||||||
"p" => "png",
|
|
||||||
"g" => "gif",
|
|
||||||
_ => "jpg"
|
|
||||||
};
|
|
||||||
|
|
||||||
private Gallery ModelToGallery(NhentaiApiModel.Gallery model)
|
|
||||||
{
|
|
||||||
var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb."
|
|
||||||
+ GetNhentaiExtensionInternal(model.Images.Thumbnail.T);
|
|
||||||
|
|
||||||
var url = $"https://nhentai.net/g/{model.Id}";
|
|
||||||
return new Gallery(
|
|
||||||
model.Id.ToString(),
|
|
||||||
url,
|
|
||||||
model.Title.English,
|
|
||||||
model.Title.Pretty,
|
|
||||||
thumbnail,
|
|
||||||
model.NumPages,
|
|
||||||
model.NumFavorites,
|
|
||||||
model.UploadDate.ToUnixTimestamp().UtcDateTime,
|
|
||||||
model.Tags.Map(x => new Tag()
|
|
||||||
{
|
|
||||||
Name = x.Name,
|
|
||||||
Url = "https://nhentai.com/" + x.Url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<NhentaiApiModel.Gallery> GetNhentaiByIdInternalAsync(uint id)
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id);
|
|
||||||
return JsonConvert.DeserializeObject<NhentaiApiModel.Gallery>(res);
|
|
||||||
}
|
|
||||||
catch (HttpRequestException)
|
|
||||||
{
|
|
||||||
Log.Warning("Nhentai with id {NhentaiId} not found", id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NhentaiApiModel.Gallery[]> SearchNhentaiInternalAsync(string search)
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search);
|
|
||||||
return JsonConvert.DeserializeObject<NhentaiApiModel.SearchResult>(res).Result;
|
|
||||||
}
|
|
||||||
catch (HttpRequestException)
|
|
||||||
{
|
|
||||||
Log.Warning("Nhentai with search {NhentaiSearch} not found", search);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Gallery> GetNhentaiByIdAsync(uint id)
|
|
||||||
{
|
|
||||||
var model = await GetNhentaiByIdInternalAsync(id);
|
|
||||||
|
|
||||||
return ModelToGallery(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly string[] _bannedTags =
|
|
||||||
{
|
|
||||||
"loli",
|
|
||||||
"lolicon",
|
|
||||||
"shota",
|
|
||||||
"shotacon",
|
|
||||||
"cub"
|
|
||||||
};
|
|
||||||
|
|
||||||
public async Task<Gallery> GetNhentaiBySearchAsync(string search)
|
|
||||||
{
|
|
||||||
var models = await SearchNhentaiInternalAsync(search);
|
|
||||||
|
|
||||||
models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray();
|
|
||||||
|
|
||||||
if (models.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ModelToGallery(models[_rng.Next(0, models.Length)]);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,134 @@
|
|||||||
using Newtonsoft.Json.Linq;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Utility.Common.Patreon
|
namespace NadekoBot.Modules.Utility.Common.Patreon
|
||||||
{
|
{
|
||||||
public class PatreonData
|
public sealed class Attributes
|
||||||
{
|
{
|
||||||
public JObject[] Included { get; set; }
|
[JsonPropertyName("full_name")]
|
||||||
public JObject[] Data { get; set; }
|
public string FullName { get; set; }
|
||||||
public PatreonDataLinks Links { get; set; }
|
|
||||||
|
[JsonPropertyName("is_follower")]
|
||||||
|
public bool IsFollower { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("last_charge_date")]
|
||||||
|
public DateTime LastChargeDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("last_charge_status")]
|
||||||
|
public string LastChargeStatus { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lifetime_support_cents")]
|
||||||
|
public int LifetimeSupportCents { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("currently_entitled_amount_cents")]
|
||||||
|
public int CurrentlyEntitledAmountCents { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("patron_status")]
|
||||||
|
public string PatronStatus { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PatreonDataLinks
|
public sealed class Data
|
||||||
{
|
{
|
||||||
public string first { get; set; }
|
[JsonPropertyName("id")]
|
||||||
public string next { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PatreonUserAndReward
|
public sealed class Address
|
||||||
{
|
{
|
||||||
public PatreonUser User { get; set; }
|
[JsonPropertyName("data")]
|
||||||
public PatreonPledge Reward { get; set; }
|
public Data Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// public sealed class CurrentlyEntitledTiers
|
||||||
|
// {
|
||||||
|
// [JsonPropertyName("data")]
|
||||||
|
// public List<Datum> Data { get; set; }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public sealed class Relationships
|
||||||
|
// {
|
||||||
|
// [JsonPropertyName("address")]
|
||||||
|
// public Address Address { get; set; }
|
||||||
|
//
|
||||||
|
// // [JsonPropertyName("currently_entitled_tiers")]
|
||||||
|
// // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; }
|
||||||
|
// }
|
||||||
|
|
||||||
|
public sealed class PatreonResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public List<PatreonMember> Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("included")]
|
||||||
|
public List<PatreonUser> Included { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("links")]
|
||||||
|
public PatreonLinks Links { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonLinks
|
||||||
|
{
|
||||||
|
[JsonPropertyName("next")]
|
||||||
|
public string Next { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonUser
|
||||||
|
{
|
||||||
|
[JsonPropertyName("attributes")]
|
||||||
|
public PatreonUserAttributes Attributes { get; set; }
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
// public string Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonUserAttributes
|
||||||
|
{
|
||||||
|
[JsonPropertyName("social_connections")]
|
||||||
|
public PatreonSocials SocialConnections { get; set; }
|
||||||
|
}
|
||||||
|
public sealed class PatreonSocials
|
||||||
|
{
|
||||||
|
[JsonPropertyName("discord")]
|
||||||
|
public DiscordSocial Discord { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DiscordSocial
|
||||||
|
{
|
||||||
|
[JsonPropertyName("user_id")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonMember
|
||||||
|
{
|
||||||
|
[JsonPropertyName("attributes")]
|
||||||
|
public Attributes Attributes { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("relationships")]
|
||||||
|
public Relationships Relationships { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class Relationships
|
||||||
|
{
|
||||||
|
[JsonPropertyName("user")]
|
||||||
|
public PatreonRelationshipUser User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonRelationshipUser
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public PatreonUserData Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatreonUserData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,62 +0,0 @@
|
|||||||
namespace NadekoBot.Modules.Utility.Common.Patreon
|
|
||||||
{
|
|
||||||
public class Attributes
|
|
||||||
{
|
|
||||||
public int amount_cents { get; set; }
|
|
||||||
public string created_at { get; set; }
|
|
||||||
public object declined_since { get; set; }
|
|
||||||
public bool is_twitch_pledge { get; set; }
|
|
||||||
public bool patron_pays_fees { get; set; }
|
|
||||||
public int? pledge_cap_cents { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Address
|
|
||||||
{
|
|
||||||
public object data { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Data
|
|
||||||
{
|
|
||||||
public string id { get; set; }
|
|
||||||
public string type { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Links
|
|
||||||
{
|
|
||||||
public string related { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Creator
|
|
||||||
{
|
|
||||||
public Data data { get; set; }
|
|
||||||
public Links links { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Patron
|
|
||||||
{
|
|
||||||
public Data data { get; set; }
|
|
||||||
public Links links { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Reward
|
|
||||||
{
|
|
||||||
public Data data { get; set; }
|
|
||||||
public Links links { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Relationships
|
|
||||||
{
|
|
||||||
public Address address { get; set; }
|
|
||||||
public Creator creator { get; set; }
|
|
||||||
public Patron patron { get; set; }
|
|
||||||
public Reward reward { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PatreonPledge
|
|
||||||
{
|
|
||||||
public Attributes attributes { get; set; }
|
|
||||||
public string id { get; set; }
|
|
||||||
public Relationships relationships { get; set; }
|
|
||||||
public string type { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user