mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
b61b1dbfaa | ||
|
92365fd22d | ||
|
24a4745193 | ||
|
1af75fd813 | ||
|
18160164eb | ||
|
2fd7d97025 | ||
|
6ada15049d | ||
|
0ebc40b95c | ||
|
02de25a931 | ||
|
0b395e9176 | ||
|
4532f992cd | ||
|
34201f0558 | ||
|
d2f4d63183 | ||
|
b41c014869 | ||
|
d348347762 | ||
|
db7cf3d757 | ||
|
83ea046d5f | ||
|
d5c94424e9 | ||
|
ff95b3d00f | ||
|
4b5fa3bb04 |
49
CHANGELOG.md
49
CHANGELOG.md
@@ -4,6 +4,55 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [3.0.9] - 21.11.2021
|
||||
|
||||
### Added
|
||||
- Added `.emojiadd` with 3 overloads
|
||||
- `.ea :customEmoji:` which copies another server's emoji
|
||||
- `.ea newName :customEmoji:` which copies emoji under a different name
|
||||
- `.ea emojiName <imagelink.png>` which creates a new emoji from the specified image
|
||||
- Patreon Access and Refresh Tokens should now be automatically updated once a month as long as the user has provided the necessary credentials in creds.yml file:
|
||||
- `Patreon.ClientId`
|
||||
- `Patreon.RefreshToken` (will also get updated once a month but needs an initial value)
|
||||
- `Patreon.ClientSecret`
|
||||
- `Patreon.CampaignId`
|
||||
|
||||
### Fixed
|
||||
- Fixed an error that would show up in the console when a club image couldn't be drawn in certain circumstances
|
||||
|
||||
## [3.0.8] - 03.11.2021
|
||||
|
||||
### Added
|
||||
- Created VotesApi project nad re-worked vote rewards handling
|
||||
- Updated votes entries in creds.yml with explanations on how to set up vote links
|
||||
|
||||
### Fixed
|
||||
- Fixed adding currency to users who don't exist in the database
|
||||
- Memory used by the bot is now correct (thanks to kotz)
|
||||
- Ban/kick will no longer fail due to too long reasons
|
||||
- Fixed some fields not preserving inline after string replacements
|
||||
|
||||
### Changed
|
||||
- `images.json` moved to `images.yml`
|
||||
- Links will use the new cdn url
|
||||
- Heads and Tails images will be updated if you haven't changed them already
|
||||
- `.slot` redesigned (and updated entries in `images.yml`)
|
||||
- Reduced required permissions for .qdel (thanks to tbodt)
|
||||
|
||||
## [3.0.7] - 05.10.2021
|
||||
|
||||
### Added
|
||||
- `.streamsclear` re-added. It will remove all followed streams on the server.
|
||||
- `.gifts` now have 3 new ✂️ Haircut 🧻 ToiletPaper and 🥀 WiltedRose which **reduce** waifu's value
|
||||
- They are called negative gifts
|
||||
- They show up at the end of the `.gifts` page and are marked with a broken heart
|
||||
- They have a separate multiplier (`waifu.multi.negative_gift_effect` default 0.5, changeable via `.config gambling` or `data/gambling.yml`)
|
||||
- When gifted, the waifu's price will be reduced by the `price * multiplier`
|
||||
- Negative gifts don't show up in `.waifuinfo` nor is the record of them kept in the database
|
||||
|
||||
### Fixed
|
||||
- Fixed `%users%` and `%shard.usercount%` placeholders not showing correct values
|
||||
|
||||
## [3.0.6] - 27.09.2021
|
||||
|
||||
### Added
|
||||
|
@@ -24,10 +24,11 @@ WORKDIR /app
|
||||
RUN set -xe; \
|
||||
useradd -m nadeko; \
|
||||
apt-get update; \
|
||||
apt-get install -y libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 sudo; \
|
||||
apt-get install -y libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 python3-pip sudo; \
|
||||
update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1; \
|
||||
echo 'Defaults>nadeko env_keep+="ASPNETCORE_* DOTNET_* NadekoBot_* shard_id total_shards TZ"' > /etc/sudoers.d/nadeko; \
|
||||
curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl; \
|
||||
pip3 install --upgrade youtube-dl; \
|
||||
apt-get remove -y python3-pip; \
|
||||
chmod +x /usr/local/bin/youtube-dl
|
||||
|
||||
COPY --from=build /app ./
|
||||
|
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Coordinator", "sr
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.VotesApi", "src\NadekoBot.VotesApi\NadekoBot.VotesApi.csproj", "{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -62,6 +64,12 @@ Global
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -73,6 +81,7 @@ Global
|
||||
{DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
{AE9B7F8C-81D7-4401-83A3-643B38258374} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}
|
||||
|
@@ -134,6 +134,8 @@ Compared to using tmux, this method requires a little bit more work to set up, b
|
||||
echo "[Unit]
|
||||
Description=NadekoBot service
|
||||
After=network.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=2
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -144,10 +146,11 @@ Compared to using tmux, this method requires a little bit more work to set up, b
|
||||
# source code.
|
||||
#ExecStartPre=/usr/bin/dotnet build ../src/NadekoBot/NadekoBot.csproj -c Release -o output/
|
||||
ExecStart=/usr/bin/dotnet NadekoBot.dll
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=NadekoBot
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" | sudo tee /etc/systemd/system/nadeko.service
|
||||
@@ -172,12 +175,16 @@ This method is similar to the one above, but requires one extra step, with the a
|
||||
echo "[Unit]
|
||||
Description=NadekoBot service
|
||||
After=network.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=2
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$USER
|
||||
WorkingDirectory=$PWD
|
||||
WorkingDirectory=$_WORKING_DIR
|
||||
ExecStart=/bin/bash NadekoRun.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=NadekoBot
|
||||
@@ -191,14 +198,14 @@ This method is similar to the one above, but requires one extra step, with the a
|
||||
4. Use the following command to create a script that will be used to start Nadeko:
|
||||
|
||||
```bash
|
||||
echo "#\!/bin/bash
|
||||
|
||||
echo \"\"
|
||||
echo \"Running NadekoBot in the background with auto restart\"
|
||||
{
|
||||
echo '#!/bin/bash'
|
||||
echo ""
|
||||
echo "echo \"Running NadekoBot in the background with auto restart\"
|
||||
youtube-dl -U
|
||||
|
||||
# If you want Nadeko to be compiled prior to every startup, uncomment the lines
|
||||
# below. Note that it's not neccessary unless you are personally modifying the
|
||||
# below. Note that it's not necessary unless you are personally modifying the
|
||||
# source code.
|
||||
#echo \"Compiling NadekoBot...\"
|
||||
#cd \"$PWD\"/nadekobot
|
||||
@@ -207,23 +214,33 @@ This method is similar to the one above, but requires one extra step, with the a
|
||||
echo \"Starting NadekoBot...\"
|
||||
|
||||
while true; do
|
||||
{
|
||||
cd \"$PWD\"/nadekobot/output
|
||||
dotnet NadekoBot.dll
|
||||
## If a non-zero exit code is produced, exit this script.
|
||||
} || {
|
||||
error_code=\"\$?\"
|
||||
if [[ -d $PWD/nadekobot/output ]]; then
|
||||
cd $PWD/nadekobot/output || {
|
||||
echo \"Failed to change working directory to $PWD/nadekobot/output\" >&2
|
||||
echo \"Ensure that the working directory inside of '/etc/systemd/system/nadeko.service' is correct\"
|
||||
echo \"Exiting...\"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo \"$PWD/nadekobot/output doesn't exist\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dotnet NadekoBot.dll || {
|
||||
echo \"An error occurred when trying to start NadekBot\"
|
||||
echo \"EXIT CODE: \$?\"
|
||||
exit \"\$error_code\"
|
||||
echo \"Exiting...\"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
echo \"Waiting for 5 seconds...\"
|
||||
sleep 5
|
||||
youtube-dl -U
|
||||
echo \"Restarting NadekoBot...\"
|
||||
done
|
||||
|
||||
echo \"Stopping NadekoBot...\"" > NadekoRun.sh
|
||||
echo \"Stopping NadekoBot...\""
|
||||
} > NadekoRun.sh
|
||||
```
|
||||
|
||||
5. Start Nadeko:
|
||||
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
|
||||
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
|
||||
|
@@ -21,7 +21,7 @@
|
||||
#### Prerequisites
|
||||
|
||||
- Windows 8 or later (64-bit)
|
||||
- [Create a Discord Bot application and invite the bot to your server](../../creds-guide.md)
|
||||
- [Create a Discord Bot application and invite the bot to your server](../creds-guide.md)
|
||||
|
||||
**Optional**
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
- Download and run the [NadekoBot v3 Updater][Updater].
|
||||
- Click on the + at the top left to create a new bot.
|
||||

|
||||

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

|
||||

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

|
||||

|
||||
- Click on **`Install`** next to **`Redis`**.
|
||||
- **Note: If Redis fails to install, install Redis manually here: [Redis Installer](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.0.504) Download and run the **`.msi`** file.
|
||||
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DL`**.
|
||||
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
|
||||
- When installation is finished, click on **`CREDS`** to the left of **`RUN`** at the lower right.
|
||||
|
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 CommandService _commandService;
|
||||
private readonly DbService _db;
|
||||
private readonly BotCredsProvider _credsProvider;
|
||||
private readonly IBotCredsProvider _credsProvider;
|
||||
|
||||
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
||||
|
||||
@@ -95,8 +95,8 @@ namespace NadekoBot
|
||||
}
|
||||
|
||||
var svcs = new ServiceCollection()
|
||||
.AddTransient<IBotCredentials>(_ => _creds) // bot creds
|
||||
.AddSingleton(_credsProvider)
|
||||
.AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
|
||||
.AddSingleton<IBotCredsProvider>(_credsProvider)
|
||||
.AddSingleton(_db) // database
|
||||
.AddRedis(_creds.RedisOptions) // redis
|
||||
.AddSingleton(Client) // discord socket client
|
||||
@@ -145,7 +145,8 @@ namespace NadekoBot
|
||||
|
||||
svcs.Scan(scan => scan
|
||||
.FromAssemblyOf<IReadyExecutor>()
|
||||
.AddClasses(classes => classes.AssignableToAny(
|
||||
.AddClasses(classes => classes
|
||||
.AssignableToAny(
|
||||
// services
|
||||
typeof(INService),
|
||||
|
||||
|
@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Attributes
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
|
||||
{
|
||||
var creds = services.GetRequiredService<BotCredsProvider>().GetCreds();
|
||||
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
|
||||
|
||||
return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner")));
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ namespace NadekoBot.Common
|
||||
OwnerIds = new List<ulong>();
|
||||
TotalShards = 1;
|
||||
GoogleApiKey = string.Empty;
|
||||
Votes = new(string.Empty, string.Empty);
|
||||
Votes = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
BotListToken = string.Empty;
|
||||
CleverbotApiKey = string.Empty;
|
||||
@@ -73,16 +73,6 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
|
||||
Change only if you've changed the coordinator address or port.")]
|
||||
public string CoordinatorUrl { get; set; }
|
||||
|
||||
[YamlIgnore]
|
||||
public string PatreonCampaignId => Patreon?.CampaignId;
|
||||
[YamlIgnore]
|
||||
public string PatreonAccessToken => Patreon?.AccessToken;
|
||||
|
||||
[YamlIgnore]
|
||||
public string VotesUrl => Votes?.Url;
|
||||
[YamlIgnore]
|
||||
public string VotesToken => Votes.Key;
|
||||
|
||||
[Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
|
||||
public string RapidApiKey { get; set; }
|
||||
|
||||
@@ -126,11 +116,9 @@ Windows default
|
||||
// todo fixup patreon
|
||||
public sealed record PatreonSettings
|
||||
{
|
||||
[Comment(@"Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal")]
|
||||
public string ClientId { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
[Comment(@"Unused atm")]
|
||||
public string RefreshToken { get; set; }
|
||||
[Comment(@"Unused atm")]
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
[Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
|
||||
@@ -143,19 +131,44 @@ Windows default
|
||||
ClientSecret = clientSecret;
|
||||
CampaignId = campaignId;
|
||||
}
|
||||
|
||||
public PatreonSettings()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record VotesSettings
|
||||
{
|
||||
[Comment(@"")]
|
||||
public string Url { get; set; }
|
||||
[Comment(@"")]
|
||||
public string Key { get; set; }
|
||||
[Comment(@"top.gg votes service url
|
||||
This is the url of your instance of the NadekoBot.Votes api
|
||||
Example: https://votes.my.cool.bot.com")]
|
||||
public string TopggServiceUrl { get; set; }
|
||||
|
||||
[Comment(@"Authorization header value sent to the TopGG service url with each request
|
||||
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file")]
|
||||
public string TopggKey { get; set; }
|
||||
|
||||
[Comment(@"discords.com votes service url
|
||||
This is the url of your instance of the NadekoBot.Votes api
|
||||
Example: https://votes.my.cool.bot.com")]
|
||||
public string DiscordsServiceUrl { get; set; }
|
||||
|
||||
[Comment(@"Authorization header value sent to the Discords service url with each request
|
||||
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file")]
|
||||
public string DiscordsKey { get; set; }
|
||||
|
||||
public VotesSettings(string url, string key)
|
||||
public VotesSettings()
|
||||
{
|
||||
Url = url;
|
||||
Key = key;
|
||||
|
||||
}
|
||||
|
||||
public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey)
|
||||
{
|
||||
TopggServiceUrl = topggServiceUrl;
|
||||
TopggKey = topggKey;
|
||||
DiscordsServiceUrl = discordsServiceUrl;
|
||||
DiscordsKey = discordsKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,16 +12,14 @@ namespace NadekoBot
|
||||
string GoogleApiKey { get; }
|
||||
ICollection<ulong> OwnerIds { get; }
|
||||
string RapidApiKey { get; }
|
||||
string PatreonAccessToken { get; }
|
||||
|
||||
Creds.DbOptions Db { get; }
|
||||
string OsuApiKey { get; }
|
||||
int TotalShards { get; }
|
||||
string PatreonCampaignId { get; }
|
||||
Creds.PatreonSettings Patreon { get; }
|
||||
string CleverbotApiKey { get; }
|
||||
RestartConfig RestartCommand { get; }
|
||||
string VotesUrl { get; }
|
||||
string VotesToken { get; }
|
||||
Creds.VotesSettings Votes { get; }
|
||||
string BotListToken { get; }
|
||||
string RedisOptions { get; }
|
||||
string LocationIqApiKey { get; }
|
||||
|
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using NadekoBot.Common.Yml;
|
||||
|
||||
namespace NadekoBot.Common
|
||||
{
|
||||
public class ImageUrls
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public CoinData Coins { get; set; }
|
||||
|
49
src/NadekoBot/Common/OldImageUrls.cs
Normal file
49
src/NadekoBot/Common/OldImageUrls.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Common
|
||||
{
|
||||
public class OldImageUrls
|
||||
{
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public CoinData Coins { get; set; }
|
||||
public Uri[] Currency { get; set; }
|
||||
public Uri[] Dice { get; set; }
|
||||
public RategirlData Rategirl { get; set; }
|
||||
public XpData Xp { get; set; }
|
||||
|
||||
//new
|
||||
public RipData Rip { get; set; }
|
||||
public SlotData Slots { get; set; }
|
||||
|
||||
public class RipData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
public Uri Overlay { get; set; }
|
||||
}
|
||||
|
||||
public class SlotData
|
||||
{
|
||||
public Uri[] Emojis { get; set; }
|
||||
public Uri[] Numbers { get; set; }
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
|
||||
public class CoinData
|
||||
{
|
||||
public Uri[] Heads { get; set; }
|
||||
public Uri[] Tails { get; set; }
|
||||
}
|
||||
|
||||
public class RategirlData
|
||||
{
|
||||
public Uri Matrix { get; set; }
|
||||
public Uri Dot { get; set; }
|
||||
}
|
||||
|
||||
public class XpData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -177,13 +177,13 @@ namespace NadekoBot.Common.Replacements
|
||||
/*OBSOLETE*/
|
||||
_reps.TryAdd("%servers%", () => c.Guilds.Count.ToString());
|
||||
#if !GLOBAL_NADEKO
|
||||
_reps.TryAdd("%users%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
|
||||
_reps.TryAdd("%users%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
#endif
|
||||
|
||||
/*NEW*/
|
||||
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
|
||||
#if !GLOBAL_NADEKO
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
#endif
|
||||
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
|
||||
return this;
|
||||
|
@@ -69,6 +69,7 @@ namespace NadekoBot.Common.Replacements
|
||||
var newF = new SmartTextEmbedField();
|
||||
newF.Name = Replace(f.Name);
|
||||
newF.Value = Replace(f.Value);
|
||||
newF.Inline = f.Inline;
|
||||
fields.Add(newF);
|
||||
}
|
||||
|
||||
|
17
src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs
Normal file
17
src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace NadekoBot.Common.TypeReaders
|
||||
{
|
||||
public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
|
||||
{
|
||||
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
if (!Emote.TryParse(input, out var emote))
|
||||
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid emote"));
|
||||
|
||||
return Task.FromResult(TypeReaderResult.FromSuccess(emote));
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@ namespace NadekoBot.Common.Yml
|
||||
.WithTypeConverter(new Rgba32Converter())
|
||||
.WithTypeConverter(new CultureInfoConverter())
|
||||
.WithTypeConverter(new UriConverter())
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
}
|
||||
}
|
@@ -138,14 +138,15 @@ WHERE UserId={userId};");
|
||||
// just update the amount, there is no new user data
|
||||
if (!updatedUserData)
|
||||
{
|
||||
ctx.Database.ExecuteSqlInterpolated($@"
|
||||
var rows = ctx.Database.ExecuteSqlInterpolated($@"
|
||||
UPDATE OR IGNORE DiscordUser
|
||||
SET CurrencyAmount=CurrencyAmount+{amount}
|
||||
WHERE UserId={userId};
|
||||
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
|
||||
");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -157,8 +158,8 @@ SET CurrencyAmount=CurrencyAmount+{amount},
|
||||
AvatarId={avatarId}
|
||||
WHERE UserId={userId};
|
||||
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
|
||||
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
|
||||
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
|
||||
");
|
||||
}
|
||||
return true;
|
||||
|
@@ -54,7 +54,6 @@ namespace NadekoBot.Db
|
||||
.Include(gc => gc.CommandCooldowns)
|
||||
.Include(gc => gc.FollowedStreams)
|
||||
.Include(gc => gc.StreamRole)
|
||||
.Include(gc => gc.NsfwBlacklistedTags)
|
||||
.Include(gc => gc.XpSettings)
|
||||
.ThenInclude(x => x.ExclusionList)
|
||||
.Include(gc => gc.DelMsgOnCmdChannels)
|
||||
|
@@ -91,7 +91,6 @@ namespace NadekoBot.Services.Database.Models
|
||||
public bool WarningsInitialized { get; set; }
|
||||
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
||||
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
|
||||
public HashSet<NsfwBlacklitedTag> NsfwBlacklistedTags { get; set; } = new HashSet<NsfwBlacklitedTag>();
|
||||
|
||||
public List<ShopEntry> ShopEntries { get; set; }
|
||||
public ulong? GameVoiceChannel { get; set; } = null;
|
||||
|
16
src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs
Normal file
16
src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Services.Database.Models
|
||||
{
|
||||
public class NsfwBlacklistedTag : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public string Tag { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Tag.GetHashCode(StringComparison.InvariantCulture);
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is NsfwBlacklistedTag x && x.Tag == Tag;
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Services.Database.Models
|
||||
{
|
||||
public class NsfwBlacklitedTag : DbEntity
|
||||
{
|
||||
public string Tag { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Tag.GetHashCode(StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is NsfwBlacklitedTag x
|
||||
? x.Tag == Tag
|
||||
: false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -59,6 +59,7 @@ namespace NadekoBot.Services.Database
|
||||
public DbSet<Poll> Poll { get; set; }
|
||||
public DbSet<WaifuInfo> WaifuInfo { get; set; }
|
||||
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
|
||||
public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
|
||||
|
||||
public NadekoContext(DbContextOptions<NadekoContext> options) : base(options)
|
||||
{
|
||||
@@ -357,6 +358,10 @@ namespace NadekoBot.Services.Database
|
||||
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc
|
||||
.HasIndex(x => x.ChannelId)
|
||||
.IsUnique());
|
||||
|
||||
modelBuilder.Entity<NsfwBlacklistedTag>(nbt => nbt
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,12 @@ namespace NadekoBot.Migrations
|
||||
migrationBuilder.Sql("DELETE FROM FilterChannelId WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
|
||||
migrationBuilder.Sql("DELETE FROM CommandCooldown WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
|
||||
|
||||
// fix for users who edited their waifuinfo table manually and are unable to update
|
||||
migrationBuilder.Sql("DELETE FROM WaifuInfo where WaifuId not in (SELECT Id from DiscordUser);");
|
||||
|
||||
// fix for users who deleted clubs manually and are unable to update now
|
||||
migrationBuilder.Sql("UPDATE DiscordUser SET ClubId = null WHERE ClubId is not null and ClubId not in (SELECT Id from Clubs);");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChannelCreated",
|
||||
table: "LogSettings");
|
||||
|
2648
src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs
generated
Normal file
2648
src/NadekoBot/Migrations/20211015232708_nsfw-blacklist-tags.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace NadekoBot.Migrations
|
||||
{
|
||||
public partial class nsfwblacklisttags : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NsfwBlacklistedTags",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Tag = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NsfwBlacklistedTags", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NsfwBlacklistedTags_GuildId",
|
||||
table: "NsfwBlacklistedTags",
|
||||
column: "GuildId");
|
||||
|
||||
migrationBuilder.Sql(@"INSERT INTO NsfwBlacklistedTags(Id, GuildId, Tag, DateAdded)
|
||||
SELECT
|
||||
Id,
|
||||
(SELECT GuildId From GuildConfigs WHERE Id=GuildConfigId),
|
||||
Tag,
|
||||
DateAdded
|
||||
FROM NsfwBlacklitedTag
|
||||
WHERE GuildConfigId in (SELECT Id from GuildConfigs);");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NsfwBlacklitedTag");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NsfwBlacklistedTags");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NsfwBlacklitedTag",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Tag = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NsfwBlacklitedTag", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NsfwBlacklitedTag_GuildConfigs_GuildConfigId",
|
||||
column: x => x.GuildConfigId,
|
||||
principalTable: "GuildConfigs",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NsfwBlacklitedTag_GuildConfigId",
|
||||
table: "NsfwBlacklitedTag",
|
||||
column: "GuildConfigId");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1062,7 +1062,7 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("MutedUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -1071,7 +1071,7 @@ namespace NadekoBot.Migrations
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GuildConfigId")
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
@@ -1079,9 +1079,9 @@ namespace NadekoBot.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GuildConfigId");
|
||||
b.HasIndex("GuildId");
|
||||
|
||||
b.ToTable("NsfwBlacklitedTag");
|
||||
b.ToTable("NsfwBlacklistedTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
||||
@@ -2300,13 +2300,6 @@ namespace NadekoBot.Migrations
|
||||
.HasForeignKey("GuildConfigId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
|
||||
{
|
||||
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
||||
.WithMany("NsfwBlacklistedTags")
|
||||
.HasForeignKey("GuildConfigId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
|
||||
{
|
||||
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
|
||||
@@ -2573,8 +2566,6 @@ namespace NadekoBot.Migrations
|
||||
|
||||
b.Navigation("MutedUsers");
|
||||
|
||||
b.Navigation("NsfwBlacklistedTags");
|
||||
|
||||
b.Navigation("Permissions");
|
||||
|
||||
b.Navigation("ReactionRoleMessages");
|
||||
|
@@ -12,6 +12,8 @@ using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db;
|
||||
using Serilog;
|
||||
using System.Threading;
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Services
|
||||
{
|
||||
@@ -21,6 +23,11 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the (Message ID, User ID) of reaction roles that are currently being processed.
|
||||
/// </summary>
|
||||
private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new();
|
||||
|
||||
public RoleCommandsService(DiscordSocketClient client, DbService db,
|
||||
Bot bot)
|
||||
{
|
||||
@@ -38,75 +45,58 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
|
||||
private Task _client_ReactionAdded(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
if (!reaction.User.IsSpecified ||
|
||||
reaction.User.Value.IsBot ||
|
||||
reaction.User.Value is not SocketGuildUser gusr ||
|
||||
chan is not SocketGuildChannel gch ||
|
||||
!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||
return;
|
||||
|
||||
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
|
||||
|
||||
if (conf is null)
|
||||
return;
|
||||
|
||||
// compare emote names for backwards compatibility :facepalm:
|
||||
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
|
||||
|
||||
if (reactionRole != null)
|
||||
{
|
||||
if (!reaction.User.IsSpecified ||
|
||||
reaction.User.Value.IsBot ||
|
||||
!(reaction.User.Value is SocketGuildUser gusr))
|
||||
return;
|
||||
|
||||
if (!(chan is SocketGuildChannel gch))
|
||||
return;
|
||||
|
||||
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||
return;
|
||||
|
||||
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
|
||||
|
||||
if (conf is null)
|
||||
return;
|
||||
|
||||
// compare emote names for backwards compatibility :facepalm:
|
||||
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
|
||||
if (reactionRole != null)
|
||||
if (!conf.Exclusive)
|
||||
{
|
||||
if (conf.Exclusive)
|
||||
{
|
||||
var roleIds = conf.ReactionRoles.Select(x => x.RoleId)
|
||||
.Where(x => x != reactionRole.RoleId)
|
||||
.Select(x => gusr.Guild.GetRole(x))
|
||||
.Where(x => x != null);
|
||||
|
||||
var __ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
//if the role is exclusive,
|
||||
// remove all other reactions user added to the message
|
||||
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
foreach (var r in dl.Reactions)
|
||||
{
|
||||
if (r.Key.Name == reaction.Emote.Name)
|
||||
continue;
|
||||
try { await dl.RemoveReactionAsync(r.Key, gusr).ConfigureAwait(false); } catch { }
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
await gusr.RemoveRolesAsync(roleIds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var toAdd = gusr.Guild.GetRole(reactionRole.RoleId);
|
||||
if (toAdd != null && !gusr.Roles.Contains(toAdd))
|
||||
{
|
||||
await gusr.AddRolesAsync(new[] { toAdd }).ConfigureAwait(false);
|
||||
}
|
||||
await AddReactionRoleAsync(gusr, reactionRole);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
// If same (message, user) are being processed in an exclusive rero, quit
|
||||
if (!_reacting.Add((msg.Id, reaction.UserId)))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
|
||||
new RequestOptions()
|
||||
{
|
||||
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
|
||||
}).ConfigureAwait(false);
|
||||
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
|
||||
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg, gusr, reaction, conf, reactionRole, CancellationToken.None);
|
||||
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
|
||||
|
||||
await Task.WhenAll(removeExclusiveTask, addRoleTask).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Free (message/user) for another exclusive rero
|
||||
_reacting.TryRemove((msg.Id, reaction.UserId));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
else
|
||||
{
|
||||
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
|
||||
new RequestOptions()
|
||||
{
|
||||
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
|
||||
}).ConfigureAwait(false);
|
||||
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -114,16 +104,16 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
|
||||
private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!reaction.User.IsSpecified ||
|
||||
reaction.User.Value.IsBot ||
|
||||
!(reaction.User.Value is SocketGuildUser gusr))
|
||||
reaction.User.Value is not SocketGuildUser gusr)
|
||||
return;
|
||||
|
||||
if (!(chan is SocketGuildChannel gch))
|
||||
if (chan is not SocketGuildChannel gch)
|
||||
return;
|
||||
|
||||
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
|
||||
@@ -193,5 +183,71 @@ namespace NadekoBot.Modules.Administration.Services
|
||||
uow.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reaction role to the specified user.
|
||||
/// </summary>
|
||||
/// <param name="user">A Discord guild user.</param>
|
||||
/// <param name="dbRero">The database settings of this reaction role.</param>
|
||||
private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero)
|
||||
{
|
||||
var toAdd = user.Guild.GetRole(dbRero.RoleId);
|
||||
|
||||
return (toAdd != null && !user.Roles.Contains(toAdd))
|
||||
? user.AddRoleAsync(toAdd)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the exclusive reaction roles and reactions from the specified user.
|
||||
/// </summary>
|
||||
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
|
||||
/// <param name="user">A Discord guild user.</param>
|
||||
/// <param name="reaction">The Discord reaction of the user.</param>
|
||||
/// <param name="dbReroMsg">The database entry of the reaction role message.</param>
|
||||
/// <param name="dbRero">The database settings of this reaction role.</param>
|
||||
/// <param name="cToken">A cancellation token to cancel the operation.</param>
|
||||
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
|
||||
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
|
||||
private Task RemoveExclusiveReactionRoleAsync(Cacheable<IUserMessage, ulong> reactionMessage, SocketGuildUser user, SocketReaction reaction, ReactionRoleMessage dbReroMsg, ReactionRole dbRero, CancellationToken cToken = default)
|
||||
{
|
||||
cToken.ThrowIfCancellationRequested();
|
||||
|
||||
var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId)
|
||||
.Where(x => x != dbRero.RoleId)
|
||||
.Select(x => user.Guild.GetRole(x))
|
||||
.Where(x => x != null);
|
||||
|
||||
var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken);
|
||||
|
||||
var removeRolesTask = user.RemoveRolesAsync(roleIds);
|
||||
|
||||
return Task.WhenAll(removeReactionsTask, removeRolesTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes old reactions from an exclusive reaction role.
|
||||
/// </summary>
|
||||
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
|
||||
/// <param name="user">A Discord guild user.</param>
|
||||
/// <param name="reaction">The Discord reaction of the user.</param>
|
||||
/// <param name="cToken">A cancellation token to cancel the operation.</param>
|
||||
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
|
||||
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
|
||||
private async Task RemoveOldReactionsAsync(Cacheable<IUserMessage, ulong> reactionMessage, SocketGuildUser user, SocketReaction reaction, CancellationToken cToken = default)
|
||||
{
|
||||
cToken.ThrowIfCancellationRequested();
|
||||
|
||||
//if the role is exclusive,
|
||||
// remove all other reactions user added to the message
|
||||
var dl = await reactionMessage.GetOrDownloadAsync().ConfigureAwait(false);
|
||||
foreach (var r in dl.Reactions)
|
||||
{
|
||||
if (r.Key.Name == reaction.Emote.Name)
|
||||
continue;
|
||||
try { await dl.RemoveReactionAsync(r.Key, user).ConfigureAwait(false); } catch { }
|
||||
await Task.Delay(100, cToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -450,7 +450,7 @@ namespace NadekoBot.Modules.Administration
|
||||
}
|
||||
}
|
||||
|
||||
await _mute.TimedBan(ctx.Guild, user, time.Time, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await _mute.TimedBan(ctx.Guild, user, time.Time, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
var toSend = _eb.Create().WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
.AddField(GetText(strs.username), user.ToString(), true)
|
||||
@@ -476,7 +476,7 @@ namespace NadekoBot.Modules.Administration
|
||||
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||
if (user is null)
|
||||
{
|
||||
await ctx.Guild.AddBanAsync(userId, 7, ctx.User.ToString() + " | " + msg);
|
||||
await ctx.Guild.AddBanAsync(userId, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512));
|
||||
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
@@ -516,7 +516,7 @@ namespace NadekoBot.Modules.Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await ctx.Guild.AddBanAsync(user, 7, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await ctx.Guild.AddBanAsync(user, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
|
||||
var toSend = _eb.Create().WithOkColor()
|
||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||
@@ -692,7 +692,7 @@ namespace NadekoBot.Modules.Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await ctx.Guild.AddBanAsync(user, 7, "Softban | " + ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await ctx.Guild.AddBanAsync(user, 7, ("Softban | " + ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
|
||||
catch { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
|
||||
|
||||
@@ -749,7 +749,7 @@ namespace NadekoBot.Modules.Administration
|
||||
dmFailed = true;
|
||||
}
|
||||
|
||||
await user.KickAsync(ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
|
||||
await user.KickAsync((ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
|
||||
|
||||
var toSend = _eb.Create().WithOkColor()
|
||||
.WithTitle(GetText(strs.kicked_user))
|
||||
@@ -776,23 +776,32 @@ namespace NadekoBot.Modules.Administration
|
||||
return;
|
||||
|
||||
var missing = new List<string>();
|
||||
var banning = new HashSet<IGuildUser>();
|
||||
var banning = new HashSet<IUser>();
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
foreach (var userStr in userStrings)
|
||||
{
|
||||
if (ulong.TryParse(userStr, out var userId))
|
||||
{
|
||||
var user = await ctx.Guild.GetUserAsync(userId) ??
|
||||
IUser user = await ctx.Guild.GetUserAsync(userId) ??
|
||||
await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
missing.Add(userStr);
|
||||
continue;
|
||||
// if IGuildUser is null, try to get IUser
|
||||
user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId);
|
||||
|
||||
// only add to missing if *still* null
|
||||
if (user is null)
|
||||
{
|
||||
missing.Add(userStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!await CheckRoleHierarchy(user))
|
||||
//Hierachy checks only if the user is in the guild
|
||||
if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -820,7 +829,7 @@ namespace NadekoBot.Modules.Administration
|
||||
{
|
||||
try
|
||||
{
|
||||
await toBan.BanAsync(7);
|
||||
await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@@ -149,7 +149,7 @@ namespace NadekoBot.Modules.CustomReactions
|
||||
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
|
||||
.WithDescription($"#{id}")
|
||||
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
|
||||
.AddField(GetText(strs.response), (found.Response + "\n```css\n" + found.Response).TrimTo(1020) + "```")
|
||||
.AddField(GetText(strs.response), found.Response.TrimTo(1000).Replace("](", "]\\("))
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using Cloneable;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Yml;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common
|
||||
{
|
||||
@@ -22,7 +23,7 @@ namespace NadekoBot.Modules.Gambling.Common
|
||||
}
|
||||
|
||||
[Comment(@"DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 1;
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
[Comment(@"Currency settings")]
|
||||
public CurrencyConfig Currency { get; set; }
|
||||
@@ -59,6 +60,10 @@ Set 0 for unlimited")]
|
||||
[Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
|
||||
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
|
||||
public decimal PatreonCurrencyPerCent { get; set; } = 1;
|
||||
|
||||
[Comment(@"Currency reward per vote.
|
||||
This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting")]
|
||||
public long VoteReward { get; set; } = 100;
|
||||
}
|
||||
|
||||
public class CurrencyConfig
|
||||
@@ -179,7 +184,8 @@ default is 0.02, which is 2%")]
|
||||
|
||||
public MultipliersData Multipliers { get; set; } = new MultipliersData();
|
||||
|
||||
[Comment(@"List of items available for gifting.")]
|
||||
[Comment(@"List of items available for gifting.
|
||||
If negative is true, gift will instead reduce waifu value.")]
|
||||
public List<WaifuItemModel> Items { get; set; } = new List<WaifuItemModel>();
|
||||
|
||||
public WaifuConfig()
|
||||
@@ -260,6 +266,11 @@ Default 1 (meaning no effect)")]
|
||||
Default 0.95 (meaning 95%)
|
||||
Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")]
|
||||
public decimal GiftEffect { get; set; } = 0.95M;
|
||||
|
||||
[Comment(@"What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'.
|
||||
Default 0.5 (meaning 50%)
|
||||
Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)")]
|
||||
public decimal NegativeGiftEffect { get; set; } = 0.50M;
|
||||
}
|
||||
|
||||
[Cloneable]
|
||||
@@ -268,19 +279,24 @@ Example: If a waifu is worth 1000, and she receives a gift worth 100, her new va
|
||||
public string ItemEmoji { get; set; }
|
||||
public int Price { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
|
||||
public bool Negative { get; set; }
|
||||
|
||||
public WaifuItemModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public WaifuItemModel(string itemEmoji, int price, string name)
|
||||
public WaifuItemModel(string itemEmoji, int price, string name, bool negative = false)
|
||||
{
|
||||
ItemEmoji = itemEmoji;
|
||||
Price = price;
|
||||
Name = name;
|
||||
Negative = negative;
|
||||
}
|
||||
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
|
8
src/NadekoBot/Modules/Gambling/Common/GamblingError.cs
Normal file
8
src/NadekoBot/Modules/Gambling/Common/GamblingError.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
public enum GamblingError
|
||||
{
|
||||
None,
|
||||
NotEnough
|
||||
}
|
||||
}
|
44
src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs
Normal file
44
src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.Slot
|
||||
{
|
||||
public class SlotGame
|
||||
{
|
||||
public class Result
|
||||
{
|
||||
public float Multiplier { get; }
|
||||
public int[] Rolls { get; }
|
||||
|
||||
public Result(float multiplier, int[] rolls)
|
||||
{
|
||||
Multiplier = multiplier;
|
||||
Rolls = rolls;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Random _rng = new NadekoRandom();
|
||||
|
||||
public SlotGame()
|
||||
{
|
||||
}
|
||||
|
||||
public Result Spin()
|
||||
{
|
||||
var rolls = new int[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) };
|
||||
var multi = 0;
|
||||
|
||||
if (rolls.All(x => x == 5))
|
||||
multi = 30;
|
||||
else if (rolls.All(x => x == rolls[0]))
|
||||
multi = 10;
|
||||
else if (rolls.Count(x => x == 5) == 2)
|
||||
multi = 4;
|
||||
else if (rolls.Any(x => x == 5))
|
||||
multi = 1;
|
||||
|
||||
return new Result(multi, rolls);
|
||||
}
|
||||
}
|
||||
}
|
12
src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs
Normal file
12
src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
public class SlotResponse
|
||||
{
|
||||
public float Multiplier { get; set; }
|
||||
public long Won { get; set; }
|
||||
public List<int> Rolls { get; set; } = new List<int>();
|
||||
public GamblingError Error { get; set; }
|
||||
}
|
||||
}
|
@@ -22,15 +22,12 @@ namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
private readonly IImageCache _images;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly DbService _db;
|
||||
private static readonly NadekoRandom rng = new NadekoRandom();
|
||||
|
||||
public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db,
|
||||
GamblingConfigService gss) : base(gss)
|
||||
public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss)
|
||||
{
|
||||
_images = data.LocalImages;
|
||||
_cs = cs;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
|
@@ -8,8 +8,6 @@ using System.Threading.Tasks;
|
||||
using System;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using Serilog;
|
||||
|
||||
@@ -17,76 +15,22 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
public class CurrencyEventsService : INService
|
||||
{
|
||||
public class VoteModel
|
||||
{
|
||||
public ulong User { get; set; }
|
||||
public long Date { get; set; }
|
||||
}
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly GamblingConfigService _configService;
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
|
||||
new ConcurrentDictionary<ulong, ICurrencyEvent>();
|
||||
|
||||
public CurrencyEventsService(DiscordSocketClient client,
|
||||
IBotCredentials creds, ICurrencyService cs,
|
||||
IHttpClientFactory http, GamblingConfigService configService)
|
||||
|
||||
public CurrencyEventsService(
|
||||
DiscordSocketClient client,
|
||||
ICurrencyService cs,
|
||||
GamblingConfigService configService)
|
||||
{
|
||||
_client = client;
|
||||
_cs = cs;
|
||||
_creds = creds;
|
||||
_http = http;
|
||||
_configService = configService;
|
||||
|
||||
if (_client.ShardId == 0)
|
||||
{
|
||||
Task t = BotlistUpvoteLoop();
|
||||
}
|
||||
}
|
||||
|
||||
// todo future use votes api directly?
|
||||
private async Task BotlistUpvoteLoop()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.VotesUrl))
|
||||
return;
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
|
||||
await TriggerVoteCheck().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TriggerVoteCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Get, _creds.VotesUrl))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_creds.VotesToken))
|
||||
req.Headers.Add("Authorization", _creds.VotesToken);
|
||||
using (var http = _http.CreateClient())
|
||||
using (var res = await http.SendAsync(req).ConfigureAwait(false))
|
||||
{
|
||||
if (!res.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Warning("Botlist API not reached.");
|
||||
return;
|
||||
}
|
||||
var resStr = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var ids = JsonConvert.DeserializeObject<VoteModel[]>(resStr)
|
||||
.Select(x => x.User)
|
||||
.Distinct();
|
||||
await _cs.AddBulkAsync(ids, ids.Select(x => "Voted - <https://discordbots.org/bot/nadeko/vote>"), ids.Select(x => 10L), true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error in TriggerVoteCheck");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
|
||||
@@ -127,6 +71,7 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
@@ -136,4 +81,4 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,11 @@
|
||||
using NadekoBot.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Configs;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Services;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
@@ -34,9 +38,39 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
|
||||
AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
|
||||
AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
AddParsedProp("waifu.multi.negative_gift_effect", gs => gs.Waifu.Multipliers.NegativeGiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
|
||||
AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
|
||||
private readonly IEnumerable<WaifuItemModel> antiGiftSeed = new[]
|
||||
{
|
||||
new WaifuItemModel("🥀", 100, "WiltedRose", true),
|
||||
new WaifuItemModel("✂️", 1000, "Haircut", true),
|
||||
new WaifuItemModel("🧻", 10000, "ToiletPaper", true),
|
||||
};
|
||||
|
||||
public void Migrate()
|
||||
{
|
||||
if (_data.Version < 2)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Waifu.Items = c.Waifu.Items.Concat(antiGiftSeed).ToList();
|
||||
c.Version = 2;
|
||||
});
|
||||
}
|
||||
|
||||
if (_data.Version < 3)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.VoteReward = 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,12 +6,14 @@ using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Modules.Gambling.Common.Slot;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using Serilog;
|
||||
|
||||
@@ -82,6 +84,41 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
|
||||
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SlotResponse> SlotAsync(ulong userId, long amount)
|
||||
{
|
||||
var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true);
|
||||
|
||||
if (!takeRes)
|
||||
{
|
||||
return new SlotResponse
|
||||
{
|
||||
Error = GamblingError.NotEnough
|
||||
};
|
||||
}
|
||||
|
||||
var game = new SlotGame();
|
||||
var result = game.Spin();
|
||||
long won = 0;
|
||||
|
||||
if (result.Multiplier > 0)
|
||||
{
|
||||
won = (long)(result.Multiplier * amount);
|
||||
|
||||
await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true);
|
||||
}
|
||||
|
||||
var toReturn = new SlotResponse
|
||||
{
|
||||
Multiplier = result.Multiplier,
|
||||
Won = won,
|
||||
};
|
||||
|
||||
toReturn.Rolls.AddRange(result.Rolls);
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
|
||||
public struct EconomyResult
|
||||
{
|
||||
|
122
src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs
Normal file
122
src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Services;
|
||||
using Discord.WebSocket;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
public class VoteModel
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public ulong UserId { get; set; }
|
||||
}
|
||||
|
||||
public class VoteRewardService : INService, IReadyExecutor
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ICurrencyService _currencyService;
|
||||
private readonly GamblingConfigService _gamb;
|
||||
private HttpClient _http;
|
||||
|
||||
public VoteRewardService(
|
||||
DiscordSocketClient client,
|
||||
IBotCredentials creds,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ICurrencyService currencyService,
|
||||
GamblingConfigService gamb)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_currencyService = currencyService;
|
||||
_gamb = gamb;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
if (_client.ShardId != 0)
|
||||
return;
|
||||
|
||||
_http = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
ServerCertificateCustomValidationCallback = delegate { return true; }
|
||||
});
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(30000);
|
||||
|
||||
var topggKey = _creds.Votes?.TopggKey;
|
||||
var topggServiceUrl = _creds.Votes?.TopggServiceUrl;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(topggKey)
|
||||
&& !string.IsNullOrWhiteSpace(topggServiceUrl))
|
||||
{
|
||||
_http.DefaultRequestHeaders.Authorization = new(topggKey);
|
||||
var uri = new Uri(new(topggServiceUrl), "topgg/new");
|
||||
var res = await _http.GetStringAsync(uri);
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
data.Select(_ => "top.gg vote reward"),
|
||||
data.Select(x => _gamb.Data.VoteReward),
|
||||
true);
|
||||
|
||||
Log.Information("Rewarding {Count} top.gg voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading top.gg vote rewards.");
|
||||
}
|
||||
|
||||
var discordsKey = _creds.Votes?.DiscordsKey;
|
||||
var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(discordsKey)
|
||||
&& !string.IsNullOrWhiteSpace(discordsServiceUrl))
|
||||
{
|
||||
_http.DefaultRequestHeaders.Authorization = new(discordsKey);
|
||||
var res = await _http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new"));
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
data.Select(_ => "discords.com vote reward"),
|
||||
data.Select(x => _gamb.Data.VoteReward),
|
||||
true);
|
||||
|
||||
Log.Information("Rewarding {Count} discords.com voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading discords.com vote rewards.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -394,19 +394,28 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
});
|
||||
}
|
||||
|
||||
w.Items.Add(new WaifuItem()
|
||||
if (!itemObj.Negative)
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji,
|
||||
});
|
||||
|
||||
if (w.Claimer?.UserId == from.Id)
|
||||
{
|
||||
w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
w.Items.Add(new WaifuItem()
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji,
|
||||
});
|
||||
|
||||
if (w.Claimer?.UserId == from.Id)
|
||||
{
|
||||
w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Price += itemObj.Price / 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Price += itemObj.Price / 2;
|
||||
w.Price -= (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||
if (w.Price < 1)
|
||||
w.Price = 1;
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -512,7 +521,7 @@ namespace NadekoBot.Modules.Gambling.Services
|
||||
{
|
||||
var conf = _gss.Data;
|
||||
return conf.Waifu.Items
|
||||
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name))
|
||||
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name, x.Negative))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
@@ -9,12 +9,16 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Services;
|
||||
using SixLabors.Fonts;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling
|
||||
{
|
||||
@@ -33,12 +37,16 @@ namespace NadekoBot.Modules.Gambling
|
||||
//thanks to judge for helping me with this
|
||||
|
||||
private readonly IImageCache _images;
|
||||
private readonly ICurrencyService _cs;
|
||||
private FontProvider _fonts;
|
||||
private readonly DbService _db;
|
||||
|
||||
public SlotCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gamb) : base(gamb)
|
||||
public SlotCommands(IDataCache data,
|
||||
FontProvider fonts, DbService db,
|
||||
GamblingConfigService gamb) : base(gamb)
|
||||
{
|
||||
_images = data.LocalImages;
|
||||
_cs = cs;
|
||||
_fonts = fonts;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public sealed class SlotMachine
|
||||
@@ -140,92 +148,115 @@ namespace NadekoBot.Modules.Gambling
|
||||
[NadekoCommand, Aliases]
|
||||
public async Task Slot(ShmartNumber amount)
|
||||
{
|
||||
if (!_runningUsers.Add(ctx.User.Id))
|
||||
if (!_runningUsers.Add(ctx.User.Id))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
|
||||
return;
|
||||
const int maxAmount = 9999;
|
||||
if (amount > maxAmount)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.max_bet_limit(maxAmount + CurrencySign));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
|
||||
return;
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
|
||||
if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
|
||||
return;
|
||||
}
|
||||
Interlocked.Add(ref _totalBet, amount.Value);
|
||||
using (var bgImage = Image.Load(_images.SlotBackground))
|
||||
{
|
||||
var result = SlotMachine.Pull();
|
||||
int[] numbers = result.Numbers;
|
||||
var result = await _service.SlotAsync(ctx.User.Id, amount);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions()));
|
||||
}
|
||||
}
|
||||
if (result.Error != GamblingError.None)
|
||||
{
|
||||
if (result.Error == GamblingError.NotEnough)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
|
||||
}
|
||||
|
||||
var won = amount * result.Multiplier;
|
||||
var printWon = won;
|
||||
var n = 0;
|
||||
do
|
||||
{
|
||||
var digit = (int)(printWon % 10);
|
||||
using (var img = Image.Load(_images.SlotNumbers[digit]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(img, new Point(230 - n * 16, 462), new GraphicsOptions()));
|
||||
}
|
||||
n++;
|
||||
} while ((printWon /= 10) != 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var printAmount = amount;
|
||||
n = 0;
|
||||
do
|
||||
{
|
||||
var digit = (int)(printAmount % 10);
|
||||
using (var img = Image.Load(_images.SlotNumbers[digit]))
|
||||
{
|
||||
bgImage.Mutate(x => x.DrawImage(img, new Point(395 - n * 16, 462), new GraphicsOptions()));
|
||||
}
|
||||
n++;
|
||||
} while ((printAmount /= 10) != 0);
|
||||
Interlocked.Add(ref _totalBet, amount);
|
||||
Interlocked.Add(ref _totalPaidOut, result.Won);
|
||||
|
||||
var msg = GetText(strs.better_luck);
|
||||
if (result.Multiplier != 0)
|
||||
{
|
||||
await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false);
|
||||
Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
|
||||
if (result.Multiplier == 1)
|
||||
msg = GetText(strs.slot_single(CurrencySign, 1));
|
||||
else if (result.Multiplier == 4)
|
||||
msg = GetText(strs.slot_two(CurrencySign, 4));
|
||||
else if (result.Multiplier == 10)
|
||||
msg = GetText(strs.slot_three(10));
|
||||
else if (result.Multiplier == 30)
|
||||
msg = GetText(strs.slot_jackpot(30));
|
||||
}
|
||||
long ownedAmount;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
ownedAmount = uow.Set<DiscordUser>()
|
||||
.FirstOrDefault(x => x.UserId == ctx.User.Id)
|
||||
?.CurrencyAmount ?? 0;
|
||||
}
|
||||
|
||||
using (var imgStream = bgImage.ToStream())
|
||||
{
|
||||
await ctx.Channel.SendFileAsync(imgStream, "result.png", ctx.User.Mention + " " + msg + $"\n`{GetText(strs.slot_bet)}:`{amount} `{GetText(strs.won)}:` {amount * result.Multiplier}{CurrencySign}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_runningUsers.Remove(ctx.User.Id);
|
||||
});
|
||||
}
|
||||
using (var bgImage = Image.Load<Rgba32>(_images.SlotBackground, out var format))
|
||||
{
|
||||
var numbers = new int[3];
|
||||
result.Rolls.CopyTo(numbers, 0);
|
||||
|
||||
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
|
||||
{
|
||||
TextOptions = new TextOptions()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
WrapTextWidth = 140,
|
||||
}
|
||||
}, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -317,10 +317,14 @@ namespace NadekoBot.Modules.Gambling
|
||||
.WithOkColor();
|
||||
|
||||
waifuItems
|
||||
.OrderBy(x => x.Price)
|
||||
.OrderBy(x => x.Negative)
|
||||
.ThenBy(x => x.Price)
|
||||
.Skip(9 * cur)
|
||||
.Take(9)
|
||||
.ForEach(x => embed.AddField($"{x.ItemEmoji} {x.Name}", x.Price, true));
|
||||
.ForEach(x => embed
|
||||
.AddField($"{(!x.Negative ? string.Empty : "\\💔")} {x.ItemEmoji} {x.Name}",
|
||||
Format.Bold(x.Price.ToString()) + _config.Currency.Sign,
|
||||
true));
|
||||
|
||||
return embed;
|
||||
}, waifuItems.Count, 9);
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using Discord.Commands;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Services;
|
||||
using NadekoBot.Db;
|
||||
@@ -23,6 +24,7 @@ namespace NadekoBot.Modules.Games
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NoPublicBot]
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
|
@@ -73,7 +73,6 @@ namespace NadekoBot.Modules.Games.Hangman
|
||||
return new(output);
|
||||
}
|
||||
|
||||
// todo lock
|
||||
public State Guess(string guess)
|
||||
{
|
||||
if (CurrentPhase != Phase.Running)
|
||||
|
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.Extensions;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.NSFW
|
||||
namespace NadekoBot.Modules.Nsfw
|
||||
{
|
||||
// thanks to halitalf for adding autoboob and autobutt features :D
|
||||
public class NSFW : NadekoModule<SearchesService>
|
||||
[NoPublicBot]
|
||||
public class NSFW : NadekoModule<ISearchImagesService>
|
||||
{
|
||||
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly NadekoRandom _rng;
|
||||
|
||||
public NSFW(IHttpClientFactory factory)
|
||||
{
|
||||
_httpFactory = factory;
|
||||
}
|
||||
|
||||
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);
|
||||
_rng = new NadekoRandom();
|
||||
}
|
||||
|
||||
private async Task InternalBoobs()
|
||||
@@ -76,8 +36,11 @@ namespace NadekoBot.Modules.NSFW
|
||||
JToken obj;
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -85,6 +48,7 @@ namespace NadekoBot.Modules.NSFW
|
||||
await SendErrorAsync(ex.Message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InternalButts(IMessageChannel Channel)
|
||||
{
|
||||
try
|
||||
@@ -92,8 +56,11 @@ namespace NadekoBot.Modules.NSFW
|
||||
JToken obj;
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -102,12 +69,11 @@ namespace NadekoBot.Modules.NSFW
|
||||
}
|
||||
}
|
||||
|
||||
#if !GLOBAL_NADEKO
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[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;
|
||||
|
||||
@@ -123,16 +89,18 @@ namespace NadekoBot.Modules.NSFW
|
||||
if (interval < 20)
|
||||
return;
|
||||
|
||||
var tagsArr = tags?.Split('|');
|
||||
|
||||
t = new Timer(async (state) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tagsArr is null || tagsArr.Length == 0)
|
||||
await InternalHentai(ctx.Channel, null).ConfigureAwait(false);
|
||||
if (tags is null || tags.Length == 0)
|
||||
await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false);
|
||||
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
|
||||
{
|
||||
@@ -148,7 +116,7 @@ namespace NadekoBot.Modules.NSFW
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.autohentai_started(
|
||||
interval,
|
||||
string.Join(", ", tagsArr)));
|
||||
string.Join(", ", tags)));
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
@@ -231,25 +199,24 @@ namespace NadekoBot.Modules.NSFW
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.started(interval));
|
||||
}
|
||||
#endif
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Hentai([Leftover] string tag = null) =>
|
||||
InternalHentai(ctx.Channel, tag);
|
||||
public Task Hentai(params string[] tags)
|
||||
=> InternalDapiCommand(tags, true, _service.Hentai);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[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))
|
||||
return;
|
||||
try
|
||||
{
|
||||
var images = await Task.WhenAll(_service.DapiSearch(tag, DapiSearchType.Gelbooru, ctx.Guild?.Id, true),
|
||||
_service.DapiSearch(tag, DapiSearchType.Danbooru, ctx.Guild?.Id, true),
|
||||
_service.DapiSearch(tag, DapiSearchType.Konachan, ctx.Guild?.Id, true),
|
||||
_service.DapiSearch(tag, DapiSearchType.Yandere, ctx.Guild?.Id, true)).ConfigureAwait(false);
|
||||
var images = await Task.WhenAll(_service.Yandere(ctx.Guild?.Id, true, tags),
|
||||
_service.Danbooru(ctx.Guild?.Id, true, tags),
|
||||
_service.Konachan(ctx.Guild?.Id, true, tags),
|
||||
_service.Gelbooru(ctx.Guild?.Id, true, tags));
|
||||
|
||||
var linksEnum = images?.Where(l => l != null).ToArray();
|
||||
if (images is null || !linksEnum.Any())
|
||||
@@ -258,7 +225,8 @@ namespace NadekoBot.Modules.NSFW
|
||||
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
|
||||
{
|
||||
@@ -268,43 +236,48 @@ namespace NadekoBot.Modules.NSFW
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Yandere([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.Yandere, false);
|
||||
public Task Yandere(params string[] tags)
|
||||
=> InternalDapiCommand(tags, false, _service.Yandere);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Konachan([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.Konachan, false);
|
||||
|
||||
[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);
|
||||
public Task Konachan(params string[] tags)
|
||||
=> InternalDapiCommand(tags, false, _service.Konachan);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task E621([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.E621, false);
|
||||
public Task Sankaku(params string[] tags)
|
||||
=> InternalDapiCommand(tags, false, _service.Sankaku);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Rule34([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.Rule34, false);
|
||||
public Task E621(params string[] tags)
|
||||
=> InternalDapiCommand(tags, false, _service.E621);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Danbooru([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.Danbooru, false);
|
||||
public Task Rule34(params string[] tags)
|
||||
=> InternalDapiCommand(tags, false, _service.Rule34);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Gelbooru([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.Gelbooru, false);
|
||||
public Task Danbooru(params string[] tags)
|
||||
=> InternalDapiCommand(tags, false, _service.Danbooru);
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
public Task Derpibooru([Leftover] string tag = null)
|
||||
=> InternalDapiCommand(tag, DapiSearchType.Derpibooru, false);
|
||||
public Task Gelbooru(params string[] tags)
|
||||
=> 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]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
@@ -315,8 +288,11 @@ namespace NadekoBot.Modules.NSFW
|
||||
JToken obj;
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -334,8 +310,11 @@ namespace NadekoBot.Modules.NSFW
|
||||
JToken obj;
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -351,16 +330,16 @@ namespace NadekoBot.Modules.NSFW
|
||||
{
|
||||
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),
|
||||
blTags.Any()
|
||||
? string.Join(", ", blTags)
|
||||
: "-").ConfigureAwait(false);
|
||||
? string.Join(", ", blTags)
|
||||
: "-").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
tag = tag.Trim().ToLowerInvariant();
|
||||
var added = _service.ToggleBlacklistedTag(ctx.Guild.Id, tag);
|
||||
var added = await _service.ToggleBlacklistTag(ctx.Guild.Id, tag);
|
||||
|
||||
if (added)
|
||||
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]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
@@ -394,12 +364,12 @@ namespace NadekoBot.Modules.NSFW
|
||||
|
||||
await SendNhentaiGalleryInternalAsync(g);
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")]
|
||||
[Priority(0)]
|
||||
public async Task Nhentai([Leftover]string query)
|
||||
public async Task Nhentai([Leftover] string query)
|
||||
{
|
||||
var g = await _service.GetNhentaiBySearchAsync(query);
|
||||
|
||||
@@ -420,7 +390,7 @@ namespace NadekoBot.Modules.NSFW
|
||||
.Select(tag => $"[{tag.Name}]({tag.Url})")
|
||||
.TakeWhile(tag => (count += tag.Length) < 1000)
|
||||
.JoinWith(" ");
|
||||
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithTitle(g.Title)
|
||||
.WithDescription(g.FullTitle)
|
||||
@@ -435,27 +405,23 @@ namespace NadekoBot.Modules.NSFW
|
||||
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;
|
||||
|
||||
imgObj = await _service.DapiSearch(tag, type, ctx.Guild?.Id, forceExplicit).ConfigureAwait(false);
|
||||
|
||||
if (imgObj is null)
|
||||
await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false);
|
||||
else
|
||||
var data = await func(ctx.Guild?.Id, forceExplicit, tags);
|
||||
|
||||
if (data is null || !string.IsNullOrWhiteSpace(data.Error))
|
||||
{
|
||||
var embed = _eb.Create().WithOkColor()
|
||||
.WithDescription($"{ctx.User} [{tag ?? "url"}]({imgObj}) ")
|
||||
.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 ReplyErrorLocalizedAsync(strs.no_results);
|
||||
return;
|
||||
}
|
||||
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.Collections.Generic;
|
||||
using NadekoBot.Modules.Nsfw.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class ImageCacherObject : IComparable<ImageCacherObject>
|
||||
{
|
||||
public DapiSearchType SearchType { get; }
|
||||
public Booru SearchType { get; }
|
||||
public string FileUrl { get; }
|
||||
public HashSet<string> Tags { 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;
|
||||
}
|
||||
@@ -25,7 +26,7 @@ namespace NadekoBot.Modules.Searches.Common
|
||||
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.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.Threading.Tasks;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
using NadekoBot.Modules.Nsfw.Common;
|
||||
using Serilog;
|
||||
using Configuration = AngleSharp.Configuration;
|
||||
|
||||
@@ -589,10 +590,6 @@ namespace NadekoBot.Modules.Searches
|
||||
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]
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
|
@@ -50,14 +50,6 @@ namespace NadekoBot.Modules.Searches.Services
|
||||
|
||||
public List<WoWJoke> WowJokes { get; } = new List<WoWJoke>();
|
||||
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;
|
||||
|
||||
public SearchesService(DiscordSocketClient client, IGoogleApiService google,
|
||||
@@ -75,11 +67,6 @@ namespace NadekoBot.Modules.Searches.Services
|
||||
_eb = eb;
|
||||
_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
|
||||
_client.MessageReceived += (msg) =>
|
||||
{
|
||||
@@ -366,80 +353,6 @@ namespace NadekoBot.Modules.Searches.Services
|
||||
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 int yomamaJokeIndex = 0;
|
||||
public Task<string> GetYomamaJoke()
|
||||
@@ -838,95 +751,5 @@ namespace NadekoBot.Modules.Searches.Services
|
||||
fullQueryLink,
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,8 @@ using NadekoBot.Extensions;
|
||||
using StackExchange.Redis;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Common.Replacements;
|
||||
using NadekoBot.Db;
|
||||
@@ -342,19 +344,18 @@ namespace NadekoBot.Modules.Searches.Services
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public int ClearAllStreams(ulong guildId)
|
||||
public async Task<int> ClearAllStreams(ulong guildId)
|
||||
{
|
||||
// todo future clear streams
|
||||
int count;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams));
|
||||
count = gc.FollowedStreams.Count;
|
||||
gc.FollowedStreams.Clear();
|
||||
uow.SaveChanges();
|
||||
}
|
||||
using var uow = _db.GetDbContext();
|
||||
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams));
|
||||
uow.RemoveRange(gc.FollowedStreams);
|
||||
|
||||
return count;
|
||||
foreach (var s in gc.FollowedStreams)
|
||||
await PublishUnfollowStream(s);
|
||||
|
||||
uow.SaveChanges();
|
||||
|
||||
return gc.FollowedStreams.Count;
|
||||
}
|
||||
|
||||
public async Task<FollowedStream> UnfollowStreamAsync(ulong guildId, int index)
|
||||
|
@@ -69,14 +69,14 @@ namespace NadekoBot.Modules.Searches
|
||||
fs.Type));
|
||||
}
|
||||
|
||||
// [NadekoCommand, Usage, Description, Aliases]
|
||||
// [RequireContext(ContextType.Guild)]
|
||||
// [UserPerm(GuildPerm.Administrator)]
|
||||
// public async Task StreamsClear()
|
||||
// {
|
||||
// var count = _service.ClearAllStreams(ctx.Guild.Id);
|
||||
// await ReplyErrorLocalizedAsync(strs.streams_cleared(count)));
|
||||
// }
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task StreamsClear()
|
||||
{
|
||||
var count = _service.ClearAllStreams(ctx.Guild.Id);
|
||||
await ReplyConfirmLocalizedAsync(strs.streams_cleared);
|
||||
}
|
||||
|
||||
[NadekoCommand, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
|
@@ -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
|
||||
{
|
||||
public class PatreonData
|
||||
public sealed class Attributes
|
||||
{
|
||||
public JObject[] Included { get; set; }
|
||||
public JObject[] Data { get; set; }
|
||||
public PatreonDataLinks Links { get; set; }
|
||||
[JsonPropertyName("full_name")]
|
||||
public string FullName { 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; }
|
||||
public string next { get; set; }
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
}
|
||||
|
||||
public class PatreonUserAndReward
|
||||
public sealed class Address
|
||||
{
|
||||
public PatreonUser User { get; set; }
|
||||
public PatreonPledge Reward { get; set; }
|
||||
[JsonPropertyName("data")]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user