Compare commits

...

25 Commits
3.0.7 ... 3.0.8

Author SHA1 Message Date
Kwoth
428429ff44 Fixed creating new users in the database when awarding currency 2021-11-03 16:27:48 +01:00
Kwoth
dc344caec6 Version upped to 3.0.8 2021-11-03 15:23:45 +01:00
Kwoth
2a4d55f81d Merge branch 'v3-dev' into 'v3'
Slots redesign nad images moved to images.yml

See merge request Kwoth/nadekobot!181
2021-11-03 14:22:51 +00:00
Kwoth
d090aa23ee Slots redesign nad images moved to images.yml 2021-11-03 14:22:51 +00:00
Kwoth
65062306c6 Merge branch 'tbodt-v3-patch-66982' into 'v3'
Reduce required permissions for deleting quotes

See merge request Kwoth/nadekobot!182
2021-10-30 11:54:03 +00:00
tbodt
9ae3b66fc2 Reduce required permissions for deleting quotes 2021-10-27 01:25:40 +00:00
Kwoth
c4ba43ec6d Merge branch 'v3-dev' into 'v3'
backport of public nsfw module

See merge request Kwoth/nadekobot!176
2021-10-21 23:35:58 +00:00
Kwoth
1141791ce5 backport of public nsfw module 2021-10-21 23:35:58 +00:00
Kwoth
49f1ef7db0 Merge branch 'hokutochen-v3-patch-79292' into 'v3'
Updated images for V3 updater

See merge request Kwoth/nadekobot!179
2021-10-21 23:35:11 +00:00
Hokuto Chen
a70c35e101 Updated images for V3 updater 2021-10-21 23:35:11 +00:00
Kwoth
b61b1dbfaa Merge branch 'memfix' into 'v3'
Fixed memory counter not refreshing over time

See merge request Kwoth/nadekobot!177
2021-10-17 22:56:10 +00:00
Kaoticz
92365fd22d Fixed memory counter not refreshing over time 2021-10-17 22:56:10 +00:00
Kwoth
24a4745193 Merge branch 'v3-dev' into 'v3'
Created VotesApi project nad re-worked vote rewards handling

See merge request Kwoth/nadekobot!172
2021-10-15 22:06:30 +00:00
Kwoth
1af75fd813 Created VotesApi project nad re-worked vote rewards handling 2021-10-15 22:06:30 +00:00
Kwoth
18160164eb Merge branch 'memsize' into 'v3'
Corrected memory usage on StatusService

See merge request Kwoth/nadekobot!175
2021-10-15 22:04:30 +00:00
Kaoticz
2fd7d97025 Corrected memory usage on StatusService 2021-10-15 22:04:30 +00:00
Kwoth
6ada15049d Merge branch 'ban-kick-patch' into 'v3'
TrimTo to avoid length limit for ban/kick reasons

See merge request Kwoth/nadekobot!174
2021-10-13 17:51:08 +00:00
Alan Beatty
0ebc40b95c TrimTo to avoid length limit for ban/kick reasons 2021-10-12 17:07:46 -05:00
Kwoth
02de25a931 Typo 2021-10-09 19:55:19 +02:00
Kwoth
0b395e9176 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-10-09 19:49:26 +02:00
Kwoth
4532f992cd Fixed field not preserving inline after string replacements. closes #308 2021-10-09 19:48:54 +02:00
Kwoth
34201f0558 Merge branch 'v3' into 'v3'
Docs fix

See merge request Kwoth/nadekobot!168
2021-10-09 17:43:18 +00:00
Hunter T
d2f4d63183 Docs fix 2021-10-09 17:43:18 +00:00
Kwoth
b41c014869 Merge branch 'Kieteyuku-v3-patch-34025' into 'v3'
Use … in TrimTo to decrease necessary deletions

See merge request Kwoth/nadekobot!171
2021-10-09 17:42:24 +00:00
Ene
d348347762 Update StringExtensions.cs 2021-10-08 20:20:50 +00:00
106 changed files with 5877 additions and 1430 deletions

View File

@@ -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}

View File

@@ -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`

View File

@@ -32,12 +32,13 @@
- Download and run the [NadekoBot v3 Updater][Updater].
- Click on the + at the top left to create a new bot.
![NadekoBot Updater](https://i.imgur.com/KZV49uf.png "NadekoBot Updater")
![NadekoBot Updater](https://i.imgur.com/FmR7F7o.png "NadekoBot Updater")
- Give your bot a name and then click **`Go to setup`** at the lower right.
![Create a new bot](https://i.imgur.com/Xnp7iQL.png "Create a new bot")
![Create a new bot](https://i.imgur.com/JxtRk9e.png "Create a new bot")
- Click on **`DOWNLOAD`** at the lower right
![Bot Setup](https://i.imgur.com/6RMXNqw.png "Bot Setup")
![Bot Setup](https://i.imgur.com/HqAl36p.png "Bot Setup")
- 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.

View 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
View File

@@ -0,0 +1 @@
store/

View 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)));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.VotesApi
{
public static class ConfKeys
{
public const string DISCORDS_KEY = "DiscordsKey";
public const string TOPGG_KEY = "TopGGKey";
}
}

View 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; }
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.VotesApi
{
public static class Policies
{
public const string DiscordsAuth = "DiscordsAuth";
public const string TopggAuth = "TopggAuth";
}
}

View 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; }
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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"]

View 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>

View 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>(); });
}
}

View 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"
}
}
}
}

View 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",
...
```

View 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;
}
}
}

View 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);
}
}

View 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(); });
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace NadekoBot.VotesApi
{
public class Vote
{
public ulong UserId { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"DiscordsKey": "my_discords_key",
"TopGGKey": "my_topgg_key",
"AllowedHosts": "*"
}

View File

@@ -145,7 +145,8 @@ namespace NadekoBot
svcs.Scan(scan => scan
.FromAssemblyOf<IReadyExecutor>()
.AddClasses(classes => classes.AssignableToAny(
.AddClasses(classes => classes
.AssignableToAny(
// services
typeof(INService),

View File

@@ -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;
@@ -77,11 +77,6 @@ Change only if you've changed the coordinator address or port.")]
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; }
@@ -143,19 +138,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;
}
}

View File

@@ -20,8 +20,7 @@ namespace NadekoBot
string PatreonCampaignId { 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; }

View File

@@ -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; }

View 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; }
}
}
}

View File

@@ -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);
}

View File

@@ -20,6 +20,7 @@ namespace NadekoBot.Common.Yml
.WithTypeConverter(new Rgba32Converter())
.WithTypeConverter(new CultureInfoConverter())
.WithTypeConverter(new UriConverter())
.IgnoreUnmatchedProperties()
.Build();
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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");

View File

@@ -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))

View File

@@ -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);
}
}

View File

@@ -23,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; }
@@ -60,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

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Gambling
{
public enum GamblingError
{
None,
NotEnough
}
}

View 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);
}
}
}

View 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; }
}
}

View File

@@ -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]

View File

@@ -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;
}
}
}
}

View File

@@ -63,6 +63,14 @@ namespace NadekoBot.Modules.Gambling.Services
c.Version = 2;
});
}
if (_data.Version < 3)
{
ModifyConfig(c =>
{
c.VoteReward = 100;
});
}
}
}
}

View File

@@ -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
{

View 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.");
}
}
}
}
}

View File

@@ -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);
});
}
}
}
}

View File

@@ -0,0 +1,15 @@
namespace NadekoBot.Modules.Nsfw.Common
{
public enum Booru
{
Safebooru,
E621,
Derpibooru,
Rule34,
Gelbooru,
Konachan,
Yandere,
Danbooru,
Sankaku
}
}

View 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);
}
}

View 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;
}
}
}

View 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"));
}
}

View File

@@ -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")
{
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Nsfw.Common
{
public class E621Response
{
public List<E621Object> Posts { get; set; }
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View 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());
}
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules.Nsfw.Common
{
public interface IImageData
{
ImageData ToCachedImageData(Booru type);
}
}

View 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);
}
}

View 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());
}
}
}

View 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);
}
}

View 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());
}
}

View File

@@ -1,3 +0,0 @@
namespace NadekoBot.Modules.NSFW.Exceptions
{
}

View 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; }
}
}

View File

@@ -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))}"));
}
}
}

View File

@@ -0,0 +1,12 @@
namespace NadekoBot.Modules.Nsfw
{
public interface INsfwService
{
}
public class NsfwService
{
}
}

View 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();
}
}
}
}

View 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
}
}

View File

@@ -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;

View File

@@ -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; }
}
}

View File

@@ -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))

View File

@@ -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
}
}

View File

@@ -115,9 +115,7 @@ namespace NadekoBot.Modules.Utility
.WithOkColor()
.WithTitle(GetText(strs.quote_id($"#{data.Id}")))
.AddField(GetText(strs.trigger), data.Keyword)
.AddField(GetText(strs.response), data.Text.Length > 1000
? GetText(strs.redacted_too_long)
: Format.Sanitize(data.Text))
.AddField(GetText(strs.response), Format.Sanitize(data.Text).Replace("](", "]\\("))
.WithFooter(GetText(strs.created_by($"{data.AuthorName} ({data.AuthorId})")))
).ConfigureAwait(false);
}
@@ -205,7 +203,7 @@ namespace NadekoBot.Modules.Utility
[RequireContext(ContextType.Guild)]
public async Task QuoteDelete(int id)
{
var isAdmin = ((IGuildUser)ctx.Message.Author).GuildPermissions.Administrator;
var isAdmin = ((IGuildUser)ctx.Message.Author).GuildPermissions.ManageMessages;
var success = false;
string response;
@@ -233,7 +231,7 @@ namespace NadekoBot.Modules.Utility
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task DelAllQuotes([Leftover] string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))

View File

@@ -253,7 +253,7 @@ namespace NadekoBot.Modules.Utility
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
.AddField(GetText(strs.messages), $"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
true)
.AddField(GetText(strs.memory), $"{_stats.Heap} MB", true)
.AddField(GetText(strs.memory), FormattableString.Invariant($"{_stats.GetPrivateMemory():F2} MB"), true)
.AddField(GetText(strs.owner_ids), ownerIds, true)
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
.AddField(GetText(strs.presence),

View File

@@ -1,141 +0,0 @@
using Newtonsoft.Json.Linq;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Services.Common
{
public class ImageLoader
{
private readonly HttpClient _http;
private readonly ConnectionMultiplexer _con;
public Func<string, RedisKey> GetKey { get; }
private IDatabase _db => _con.GetDatabase();
private readonly List<Task<KeyValuePair<RedisKey, RedisValue>>> uriTasks = new List<Task<KeyValuePair<RedisKey, RedisValue>>>();
public ImageLoader(HttpClient http, ConnectionMultiplexer con, Func<string, RedisKey> getKey)
{
_http = http;
_con = con;
GetKey = getKey;
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed reading image bytes");
return null;
}
}
else
{
return await _http.GetByteArrayAsync(uri);
}
}
async Task HandleJArray(JArray arr, string key)
{
var tasks = arr.Where(x => x.Type == JTokenType.String)
.Select(async x =>
{
try
{
return await GetImageData((Uri)x).ConfigureAwait(false);
}
catch
{
Log.Error("Error retreiving image for key {Key}: {Data}", key, x);
return null;
}
});
byte[][] vals = Array.Empty<byte[]>();
vals = await Task.WhenAll(tasks).ConfigureAwait(false);
if (vals.Any(x => x is null))
vals = vals.Where(x => x != null).ToArray();
await _db.KeyDeleteAsync(GetKey(key)).ConfigureAwait(false);
await _db.ListRightPushAsync(GetKey(key),
vals.Where(x => x != null)
.Select(x => (RedisValue)x)
.ToArray()).ConfigureAwait(false);
if (arr.Count != vals.Length)
{
Log.Information("{2}/{1} URIs for the key '{0}' have been loaded. Some of the supplied URIs are either unavailable or invalid.", key, arr.Count, vals.Count());
}
}
async Task<KeyValuePair<RedisKey, RedisValue>> HandleUri(Uri uri, string key)
{
try
{
RedisValue data = await GetImageData(uri).ConfigureAwait(false);
return new KeyValuePair<RedisKey, RedisValue>(GetKey(key), data);
}
catch
{
Log.Information("Setting '{0}' image failed. The URI you provided is either unavailable or invalid.", key.ToLowerInvariant());
return new KeyValuePair<RedisKey, RedisValue>("", "");
}
}
Task HandleJObject(JObject obj, string parent = "")
{
string GetParentString()
{
if (string.IsNullOrWhiteSpace(parent))
return "";
else
return parent + "_";
}
List<Task> tasks = new List<Task>();
Task t;
// go through all of the kvps in the object
foreach (var kvp in obj)
{
// if it's a JArray, resole it using jarray method which will
// return task<byte[][]> aka an array of all images' bytes
if (kvp.Value.Type == JTokenType.Array)
{
t = HandleJArray((JArray)kvp.Value, GetParentString() + kvp.Key);
tasks.Add(t);
}
else if (kvp.Value.Type == JTokenType.String)
{
var uriTask = HandleUri((Uri)kvp.Value, GetParentString() + kvp.Key);
uriTasks.Add(uriTask);
}
else if (kvp.Value.Type == JTokenType.Object)
{
t = HandleJObject((JObject)kvp.Value, GetParentString() + kvp.Key);
tasks.Add(t);
}
}
return Task.WhenAll(tasks);
}
public async Task LoadAsync(JObject obj)
{
await HandleJObject(obj).ConfigureAwait(false);
var results = await Task.WhenAll(uriTasks).ConfigureAwait(false);
await _db.StringSetAsync(results.Where(x => x.Key != "").ToArray()).ConfigureAwait(false);
}
}
}

View File

@@ -1,52 +0,0 @@
using StackExchange.Redis;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.Services.Common
{
public sealed class RedisImageArray : IReadOnlyList<byte[]>
{
public byte[] this[int index]
{
get
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
return _con.GetDatabase().ListGetByIndex(_key, index);
}
}
public int Count => _data.IsValueCreated
? _data.Value.Length
: (int)_con.GetDatabase().ListLength(_key);
private readonly ConnectionMultiplexer _con;
private readonly string _key;
private readonly Lazy<byte[][]> _data;
public RedisImageArray(string key, ConnectionMultiplexer con)
{
_con = con;
_key = key;
_data = new Lazy<byte[][]>(() => _con.GetDatabase().ListRange(_key).Select(x => (byte[])x).ToArray(), true);
}
public IEnumerator<byte[]> GetEnumerator()
{
var actualData = _data.Value;
for (int i = 0; i < actualData.Length; i++)
{
yield return actualData[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return _data.Value.GetEnumerator();
}
}
}

View File

@@ -4,16 +4,55 @@ namespace NadekoBot.Services
{
public interface IStatsService
{
/// <summary>
/// The author of the bot.
/// </summary>
string Author { get; }
/// <summary>
/// The total amount of commands ran since startup.
/// </summary>
long CommandsRan { get; }
string Heap { get; }
/// <summary>
/// The Discord framework used by the bot.
/// </summary>
string Library { get; }
/// <summary>
/// The amount of messages seen by the bot since startup.
/// </summary>
long MessageCounter { get; }
/// <summary>
/// The rate of messages the bot sees every second.
/// </summary>
double MessagesPerSecond { get; }
/// <summary>
/// The total amount of text channels the bot can see.
/// </summary>
long TextChannels { get; }
/// <summary>
/// The total amount of voice channels the bot can see.
/// </summary>
long VoiceChannels { get; }
/// <summary>
/// Gets for how long the bot has been up since startup.
/// </summary>
TimeSpan GetUptime();
/// <summary>
/// Gets a formatted string of how long the bot has been up since startup.
/// </summary>
/// <param name="separator">The formatting separator.</param>
string GetUptimeString(string separator = ", ");
/// <summary>
/// Gets total amount of private memory currently in use by the bot, in Megabytes.
/// </summary>
double GetPrivateMemory();
}
}

View File

@@ -128,7 +128,10 @@ namespace NadekoBot.Services
null,
null,
oldCreds.PatreonCampaignId),
Votes = new Creds.VotesSettings(oldCreds.VotesUrl, oldCreds.VotesToken),
Votes = new(oldCreds.VotesUrl,
oldCreds.VotesToken,
string.Empty,
string.Empty),
BotListToken = oldCreds.BotListToken,
RedisOptions = oldCreds.RedisOptions,
LocationIqApiKey = oldCreds.LocationIqApiKey,
@@ -141,6 +144,17 @@ namespace NadekoBot.Services
Log.Warning("Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
}
if (File.Exists(_credsFileName))
{
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(_credsFileName));
if (creds.Version <= 1)
{
creds.Version = 2;
File.WriteAllText(_credsFileName, Yaml.Serializer.Serialize(creds));
}
}
}
public Creds GetCreds() => _creds;

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace NadekoBot.Services
{
@@ -48,8 +49,11 @@ namespace NadekoBot.Services
}
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
}
public FontFamily DottyFont { get; }
public FontFamily UniSans { get; }
public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; }

View File

@@ -1,181 +0,0 @@
using NadekoBot.Common;
using NadekoBot.Services.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using Serilog;
namespace NadekoBot.Services
{
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private IDatabase _db => _con.GetDatabase();
private const string _basePath = "data/";
private const string _cardsPath = "data/images/cards";
public ImageUrls ImageUrls { get; private set; }
public IReadOnlyList<byte[]> Heads => GetByteArrayData(ImageKey.Coins_Heads);
public IReadOnlyList<byte[]> Tails => GetByteArrayData(ImageKey.Coins_Tails);
public IReadOnlyList<byte[]> Dice => GetByteArrayData(ImageKey.Dice);
public IReadOnlyList<byte[]> SlotEmojis => GetByteArrayData(ImageKey.Slots_Emojis);
public IReadOnlyList<byte[]> SlotNumbers => GetByteArrayData(ImageKey.Slots_Numbers);
public IReadOnlyList<byte[]> Currency => GetByteArrayData(ImageKey.Currency);
public byte[] SlotBackground => GetByteData(ImageKey.Slots_Bg);
public byte[] RategirlMatrix => GetByteData(ImageKey.Rategirl_Matrix);
public byte[] RategirlDot => GetByteData(ImageKey.Rategirl_Dot);
public byte[] XpBackground => GetByteData(ImageKey.Xp_Bg);
public byte[] Rip => GetByteData(ImageKey.Rip_Bg);
public byte[] RipOverlay => GetByteData(ImageKey.Rip_Overlay);
public byte[] GetCard(string key)
{
return _con.GetDatabase().StringGet(GetKey("card_" + key));
}
public enum ImageKey
{
Coins_Heads,
Coins_Tails,
Dice,
Slots_Bg,
Slots_Numbers,
Slots_Emojis,
Rategirl_Matrix,
Rategirl_Dot,
Xp_Bg,
Rip_Bg,
Rip_Overlay,
Currency,
}
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new HttpClient();
ImageUrls = JsonConvert.DeserializeObject<ImageUrls>(
File.ReadAllText(Path.Combine(_basePath, "images.json")));
}
public async Task<bool> AllKeysExist()
{
try
{
var results = await Task.WhenAll(Enum.GetNames(typeof(ImageKey))
.Select(x => x.ToLowerInvariant())
.Select(x => _db.KeyExistsAsync(GetKey(x))))
.ConfigureAwait(false);
var cardsExist = await Task.WhenAll(GetAllCardNames()
.Select(x => "card_" + x)
.Select(x => _db.KeyExistsAsync(GetKey(x))))
.ConfigureAwait(false);
var num = results.Where(x => !x).Count();
return results.All(x => x) && cardsExist.All(x => x);
}
catch (Exception ex)
{
Log.Warning(ex, "Error checking for Image keys");
return false;
}
}
public async Task Reload()
{
try
{
var sw = Stopwatch.StartNew();
var obj = JObject.Parse(
File.ReadAllText(Path.Combine(_basePath, "images.json")));
ImageUrls = obj.ToObject<ImageUrls>();
var t = new ImageLoader(_http, _con, GetKey)
.LoadAsync(obj);
var loadCards = Task.Run(async () =>
{
await _db.StringSetAsync(Directory.GetFiles(_cardsPath)
.ToDictionary(
x => GetKey("card_" + Path.GetFileNameWithoutExtension(x)),
x => (RedisValue)File.ReadAllBytes(x)) // loads them and creates <name, bytes> pairs to store in redis
.ToArray())
.ConfigureAwait(false);
});
await Task.WhenAll(t, loadCards).ConfigureAwait(false);
sw.Stop();
Log.Information($"Images reloaded in {sw.Elapsed.TotalSeconds:F2}s");
}
catch (Exception ex)
{
Log.Error(ex, "Error reloading image service");
throw;
}
}
private IEnumerable<string> GetAllCardNames(bool showExtension = false)
{
return Directory.GetFiles(_cardsPath) // gets all cards from the cards folder
.Select(x => showExtension
? Path.GetFileName(x)
: Path.GetFileNameWithoutExtension(x)); // gets their names
}
public RedisKey GetKey(string key)
{
return $"{_creds.RedisKey()}_localimg_{key.ToLowerInvariant()}";
}
public byte[] GetByteData(string key)
{
return _db.StringGet(GetKey(key));
}
public byte[] GetByteData(ImageKey key) => GetByteData(key.ToString());
public RedisImageArray GetByteArrayData(string key)
{
return new RedisImageArray(GetKey(key), _con);
}
public RedisImageArray GetByteArrayData(ImageKey key) => GetByteArrayData(key.ToString());
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace NadekoBot.Services
{
public static class RedisImageExtensions
{
private const string OldCdnUrl = "nadeko-pictures.nyc3.digitaloceanspaces.com";
private const string NewCdnUrl = "cdn.nadeko.bot";
public static Uri ToNewCdn(this Uri uri)
=> new(uri.ToString().Replace(OldCdnUrl, NewCdnUrl));
}
}

View File

@@ -0,0 +1,312 @@
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Yml;
using Serilog;
namespace NadekoBot.Services
{
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
private IDatabase _db => _con.GetDatabase();
private const string _basePath = "data/";
private const string _cardsPath = "data/images/cards";
public ImageUrls ImageUrls { get; private set; }
public enum ImageKeys
{
CoinHeads,
CoinTails,
Dice,
SlotBg,
SlotEmojis,
SlotNumbers,
Currency,
RategirlMatrix,
RategirlDot,
RipOverlay,
RipBg,
XpBg
}
public IReadOnlyList<byte[]> Heads
=> GetByteArrayData(ImageKeys.CoinHeads);
public IReadOnlyList<byte[]> Tails
=> GetByteArrayData(ImageKeys.CoinTails);
public IReadOnlyList<byte[]> Dice
=> GetByteArrayData(ImageKeys.Dice);
public IReadOnlyList<byte[]> SlotEmojis
=> GetByteArrayData(ImageKeys.SlotEmojis);
public IReadOnlyList<byte[]> SlotNumbers
=> GetByteArrayData(ImageKeys.SlotNumbers);
public IReadOnlyList<byte[]> Currency
=> GetByteArrayData(ImageKeys.Currency);
public byte[] SlotBackground
=> GetByteData(ImageKeys.SlotBg);
public byte[] RategirlMatrix
=> GetByteData(ImageKeys.RategirlMatrix);
public byte[] RategirlDot
=> GetByteData(ImageKeys.RategirlDot);
public byte[] XpBackground
=> GetByteData(ImageKeys.XpBg);
public byte[] Rip
=> GetByteData(ImageKeys.RipBg);
public byte[] RipOverlay
=> GetByteData(ImageKeys.RipOverlay);
public byte[] GetCard(string key)
{
// since cards are always local for now, don't cache them
return File.ReadAllBytes(Path.Join(_cardsPath, key + ".jpg"));
}
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new HttpClient();
_imagesPath = Path.Combine(_basePath, "images.yml");
Migrate();
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
}
private void Migrate()
{
// migrate to yml
if (File.Exists(Path.Combine(_basePath, "images.json")))
{
var oldFilePath = Path.Combine(_basePath, "images.json");
var backupFilePath = Path.Combine(_basePath, "images.json.backup");
var oldData = JsonConvert.DeserializeObject<OldImageUrls>(
File.ReadAllText(oldFilePath));
if (oldData is not null)
{
var newData = new ImageUrls()
{
Coins = new ImageUrls.CoinData()
{
Heads = oldData.Coins.Heads.Length == 1 &&
oldData.Coins.Heads[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
: oldData.Coins.Heads,
Tails = oldData.Coins.Tails.Length == 1 &&
oldData.Coins.Tails[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") }
: oldData.Coins.Tails,
},
Dice = oldData.Dice.Map(x => x.ToNewCdn()),
Currency = oldData.Currency.Map(x => x.ToNewCdn()),
Rategirl = new ImageUrls.RategirlData()
{
Dot = oldData.Rategirl.Dot.ToNewCdn(),
Matrix = oldData.Rategirl.Matrix.ToNewCdn()
},
Rip = new ImageUrls.RipData()
{
Bg = oldData.Rip.Bg.ToNewCdn(),
Overlay = oldData.Rip.Overlay.ToNewCdn(),
},
Slots = new ImageUrls.SlotData()
{
Bg = new Uri("https://cdn.nadeko.bot/slots/slots_bg.png"),
Emojis = new[]
{
"https://cdn.nadeko.bot/slots/0.png",
"https://cdn.nadeko.bot/slots/1.png",
"https://cdn.nadeko.bot/slots/2.png",
"https://cdn.nadeko.bot/slots/3.png",
"https://cdn.nadeko.bot/slots/4.png",
"https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x)),
Numbers = new[]
{
"https://cdn.nadeko.bot/other/slots/numbers/0.png",
"https://cdn.nadeko.bot/other/slots/numbers/1.png",
"https://cdn.nadeko.bot/other/slots/numbers/2.png",
"https://cdn.nadeko.bot/other/slots/numbers/3.png",
"https://cdn.nadeko.bot/other/slots/numbers/4.png",
"https://cdn.nadeko.bot/other/slots/numbers/5.png",
"https://cdn.nadeko.bot/other/slots/numbers/6.png",
"https://cdn.nadeko.bot/other/slots/numbers/7.png",
"https://cdn.nadeko.bot/other/slots/numbers/8.png",
"https://cdn.nadeko.bot/other/slots/numbers/9.png"
}.Map(x => new Uri(x)),
},
Xp = new ImageUrls.XpData()
{
Bg = oldData.Xp.Bg.ToNewCdn(),
},
Version = 2,
};
File.Move(oldFilePath, backupFilePath, true);
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData));
}
}
}
public async Task Reload()
{
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(await File.ReadAllTextAsync(_imagesPath));
foreach (var key in GetAllKeys())
{
switch (key)
{
case ImageKeys.CoinHeads:
await Load(key, ImageUrls.Coins.Heads);
break;
case ImageKeys.CoinTails:
await Load(key, ImageUrls.Coins.Tails);
break;
case ImageKeys.Dice:
await Load(key, ImageUrls.Dice);
break;
case ImageKeys.SlotBg:
await Load(key, ImageUrls.Slots.Bg);
break;
case ImageKeys.SlotEmojis:
await Load(key, ImageUrls.Slots.Emojis);
break;
case ImageKeys.SlotNumbers:
await Load(key, ImageUrls.Slots.Numbers);
break;
case ImageKeys.Currency:
await Load(key, ImageUrls.Currency);
break;
case ImageKeys.RategirlMatrix:
await Load(key, ImageUrls.Rategirl.Matrix);
break;
case ImageKeys.RategirlDot:
await Load(key, ImageUrls.Rategirl.Dot);
break;
case ImageKeys.RipOverlay:
await Load(key, ImageUrls.Rip.Overlay);
break;
case ImageKeys.RipBg:
await Load(key, ImageUrls.Rip.Bg);
break;
case ImageKeys.XpBg:
await Load(key, ImageUrls.Xp.Bg);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private async Task Load(ImageKeys key, Uri uri)
{
var data = await GetImageData(uri);
if (data is null)
return;
await _db.StringSetAsync(GetRedisKey(key), data);
}
private async Task Load(ImageKeys key, Uri[] uris)
{
await _db.KeyDeleteAsync(GetRedisKey(key));
var imageData = await Task.WhenAll(uris.Select(GetImageData));
var vals = imageData
.Where(x => x is not null)
.Select(x => (RedisValue)x)
.ToArray();
await _db.ListRightPushAsync(GetRedisKey(key), vals);
if (uris.Length != vals.Length)
{
Log.Information("{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n" +
"Some of the supplied URIs are either unavailable or invalid.",
vals.Length, uris.Length, key);
}
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
return null;
}
}
try
{
return await _http.GetByteArrayAsync(uri);
}
catch (Exception ex)
{
Log.Warning(ex, "Image url you provided is not a valid image: {Uri}", uri.ToString());
return null;
}
}
private async Task<bool> AllKeysExist()
{
var tasks = await Task.WhenAll(GetAllKeys()
.Select(x => _db.KeyExistsAsync(GetRedisKey(x))));
return tasks.All(exist => exist);
}
private IEnumerable<ImageKeys> GetAllKeys() =>
Enum.GetValues<ImageKeys>();
private byte[][] GetByteArrayData(ImageKeys key)
=> _db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
private byte[] GetByteData(ImageKeys key)
=> _db.StringGet(GetRedisKey(key));
private RedisKey GetRedisKey(ImageKeys key)
=> _creds.RedisKey() + "_image_" + key;
}
}

View File

@@ -1,30 +1,28 @@
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using Serilog;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using Serilog;
namespace NadekoBot.Services
{
public class StatsService : IStatsService, IReadyExecutor, INService
public class StatsService : IStatsService, IReadyExecutor, INService, IDisposable
{
private readonly Process _currentProcess = Process.GetCurrentProcess();
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly DateTime _started;
public const string BotVersion = "3.0.7";
public const string BotVersion = "3.0.8";
public string Author => "Kwoth#2452";
public string Library => "Discord.Net";
public string Heap => Math.Round((double)GC.GetTotalMemory(false) / 1.MiB(), 2)
.ToString(CultureInfo.InvariantCulture);
public double MessagesPerSecond => MessageCounter / GetUptime().TotalSeconds;
private long _textChannels;
@@ -173,5 +171,17 @@ namespace NadekoBot.Services
_voiceChannels = guilds.Sum(g => g.Channels.Count(cx => cx is IVoiceChannel));
return Task.CompletedTask;
}
public double GetPrivateMemory()
{
_currentProcess.Refresh();
return _currentProcess.PrivateMemorySize64 / (double)1.MiB();
}
public void Dispose()
{
_currentProcess.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
@@ -50,7 +50,7 @@ namespace NadekoBot.Extensions
}
else
{
return string.Concat(str.Take(maxLength - 3)) + "...";
return string.Concat(str.Take(maxLength - 1)) + "";
}
}

View File

@@ -14,8 +14,20 @@ totalShards: 1
googleApiKey: ''
# Settings for voting system for discordbots. Meant for use on global Nadeko.
votes:
url: ''
key: ''
# top.gg votes service url
# This is the url of your instance of the NadekoBot.Votes api
# Example: https://votes.my.cool.bot.com
topggServiceUrl: ''
# 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
topggKey: ''
# discords.com votes service url
# This is the url of your instance of the NadekoBot.Votes api
# Example: https://votes.my.cool.bot.com
discordsServiceUrl: ''
# 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
discordsKey: ''
# Patreon auto reward system settings.
# go to https://www.patreon.com/portal -> my clients -> create client
patreon:

View File

@@ -1132,8 +1132,6 @@ clubbans:
clubleaderboard:
- clublb
- clubs
nsfwclearcache:
- nsfwcc
clubadmin:
- clubadmin
autoboobs:

Some files were not shown because too many files have changed in this diff Show More