mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Killed history
This commit is contained in:
207
NadekoBot.Core/Services/Impl/BotCredentials.cs
Normal file
207
NadekoBot.Core/Services/Impl/BotCredentials.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using Discord;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NadekoBot.Common;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NadekoBot.Core.Common;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class BotCredentials : IBotCredentials
|
||||
{
|
||||
public string GoogleApiKey { get; }
|
||||
public string MashapeKey { get; }
|
||||
public string Token { get; }
|
||||
|
||||
public ImmutableArray<ulong> OwnerIds { get; }
|
||||
|
||||
public string OsuApiKey { get; }
|
||||
public string CleverbotApiKey { get; }
|
||||
public RestartConfig RestartCommand { get; }
|
||||
public DBConfig Db { get; }
|
||||
public int TotalShards { get; }
|
||||
public string CarbonKey { get; }
|
||||
|
||||
private readonly string _credsFileName = Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
|
||||
public string PatreonAccessToken { get; }
|
||||
public string ShardRunCommand { get; }
|
||||
public string ShardRunArguments { get; }
|
||||
public int ShardRunPort { get; }
|
||||
|
||||
public string PatreonCampaignId { get; }
|
||||
|
||||
public string TwitchClientId { get; }
|
||||
|
||||
public string VotesUrl { get; }
|
||||
public string VotesToken { get; }
|
||||
public string BotListToken { get; }
|
||||
public string RedisOptions { get; }
|
||||
public string LocationIqApiKey { get; }
|
||||
public string TimezoneDbApiKey { get; }
|
||||
public string CoinmarketcapApiKey { get; }
|
||||
|
||||
public BotCredentials()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText("./credentials_example.json",
|
||||
JsonConvert.SerializeObject(new CredentialsModel(), Formatting.Indented));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (!File.Exists(_credsFileName))
|
||||
Log.Warning(
|
||||
$"credentials.json is missing. Attempting to load creds from environment variables prefixed with 'NadekoBot_'. Example is in {Path.GetFullPath("./credentials_example.json")}");
|
||||
try
|
||||
{
|
||||
var configBuilder = new ConfigurationBuilder();
|
||||
configBuilder.AddJsonFile(_credsFileName, true)
|
||||
.AddEnvironmentVariables("NadekoBot_");
|
||||
|
||||
var data = configBuilder.Build();
|
||||
|
||||
Token = data[nameof(Token)];
|
||||
if (string.IsNullOrWhiteSpace(Token))
|
||||
{
|
||||
Log.Error(
|
||||
"Token is missing from credentials.json or Environment variables. Add it and restart the program.");
|
||||
Helpers.ReadErrorAndExit(5);
|
||||
}
|
||||
|
||||
OwnerIds = data.GetSection("OwnerIds").GetChildren().Select(c => ulong.Parse(c.Value))
|
||||
.ToImmutableArray();
|
||||
GoogleApiKey = data[nameof(GoogleApiKey)];
|
||||
MashapeKey = data[nameof(MashapeKey)];
|
||||
OsuApiKey = data[nameof(OsuApiKey)];
|
||||
PatreonAccessToken = data[nameof(PatreonAccessToken)];
|
||||
PatreonCampaignId = data[nameof(PatreonCampaignId)] ?? "334038";
|
||||
ShardRunCommand = data[nameof(ShardRunCommand)];
|
||||
ShardRunArguments = data[nameof(ShardRunArguments)];
|
||||
CleverbotApiKey = data[nameof(CleverbotApiKey)];
|
||||
LocationIqApiKey = data[nameof(LocationIqApiKey)];
|
||||
TimezoneDbApiKey = data[nameof(TimezoneDbApiKey)];
|
||||
CoinmarketcapApiKey = data[nameof(CoinmarketcapApiKey)];
|
||||
if (string.IsNullOrWhiteSpace(CoinmarketcapApiKey))
|
||||
{
|
||||
CoinmarketcapApiKey = "e79ec505-0913-439d-ae07-069e296a6079";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data[nameof(RedisOptions)]))
|
||||
RedisOptions = data[nameof(RedisOptions)];
|
||||
else
|
||||
RedisOptions = "127.0.0.1,syncTimeout=3000";
|
||||
|
||||
VotesToken = data[nameof(VotesToken)];
|
||||
VotesUrl = data[nameof(VotesUrl)];
|
||||
BotListToken = data[nameof(BotListToken)];
|
||||
|
||||
var restartSection = data.GetSection(nameof(RestartCommand));
|
||||
var cmd = restartSection["cmd"];
|
||||
var args = restartSection["args"];
|
||||
if (!string.IsNullOrWhiteSpace(cmd))
|
||||
RestartCommand = new RestartConfig(cmd, args);
|
||||
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ShardRunCommand))
|
||||
ShardRunCommand = "dotnet";
|
||||
if (string.IsNullOrWhiteSpace(ShardRunArguments))
|
||||
ShardRunArguments = "run -c Release --no-build -- {0} {1}";
|
||||
}
|
||||
else //windows
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ShardRunCommand))
|
||||
ShardRunCommand = "NadekoBot.exe";
|
||||
if (string.IsNullOrWhiteSpace(ShardRunArguments))
|
||||
ShardRunArguments = "{0} {1}";
|
||||
}
|
||||
|
||||
var portStr = data[nameof(ShardRunPort)];
|
||||
if (string.IsNullOrWhiteSpace(portStr))
|
||||
ShardRunPort = new NadekoRandom().Next(5000, 6000);
|
||||
else
|
||||
ShardRunPort = int.Parse(portStr);
|
||||
|
||||
if (!int.TryParse(data[nameof(TotalShards)], out var ts))
|
||||
ts = 0;
|
||||
TotalShards = ts < 1 ? 1 : ts;
|
||||
|
||||
CarbonKey = data[nameof(CarbonKey)];
|
||||
var dbSection = data.GetSection("db");
|
||||
Db = new DBConfig(string.IsNullOrWhiteSpace(dbSection["Type"])
|
||||
? "sqlite"
|
||||
: dbSection["Type"],
|
||||
string.IsNullOrWhiteSpace(dbSection["ConnectionString"])
|
||||
? "Data Source=data/NadekoBot.db"
|
||||
: dbSection["ConnectionString"]);
|
||||
|
||||
TwitchClientId = data[nameof(TwitchClientId)];
|
||||
if (string.IsNullOrWhiteSpace(TwitchClientId))
|
||||
{
|
||||
TwitchClientId = "67w6z9i09xv2uoojdm9l0wsyph4hxo6";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("JSON serialization has failed. Fix your credentials file and restart the bot.");
|
||||
Log.Fatal(ex.ToString());
|
||||
Helpers.ReadErrorAndExit(6);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No idea why this thing exists
|
||||
/// </summary>
|
||||
private class CredentialsModel : IBotCredentials
|
||||
{
|
||||
public string Token { get; set; } = "";
|
||||
|
||||
public ulong[] OwnerIds { get; set; } = new ulong[]
|
||||
{
|
||||
105635576866156544
|
||||
};
|
||||
|
||||
public string GoogleApiKey { get; set; } = "";
|
||||
public string MashapeKey { get; set; } = "";
|
||||
public string OsuApiKey { get; set; } = "";
|
||||
public string SoundCloudClientId { get; set; } = "";
|
||||
public string CleverbotApiKey { get; } = "";
|
||||
public string CarbonKey { get; set; } = "";
|
||||
public DBConfig Db { get; set; } = new DBConfig("sqlite", "Data Source=data/NadekoBot.db");
|
||||
public int TotalShards { get; set; } = 1;
|
||||
public string PatreonAccessToken { get; set; } = "";
|
||||
public string PatreonCampaignId { get; set; } = "334038";
|
||||
public string RestartCommand { get; set; } = null;
|
||||
|
||||
public string ShardRunCommand { get; set; } = "";
|
||||
public string ShardRunArguments { get; set; } = "";
|
||||
public int? ShardRunPort { get; set; } = null;
|
||||
|
||||
public string BotListToken { get; set; }
|
||||
public string TwitchClientId { get; set; }
|
||||
public string VotesToken { get; set; }
|
||||
public string VotesUrl { get; set; }
|
||||
public string RedisOptions { get; set; }
|
||||
public string LocationIqApiKey { get; set; }
|
||||
public string TimezoneDbApiKey { get; set; }
|
||||
public string CoinmarketcapApiKey { get; set; }
|
||||
|
||||
[JsonIgnore] ImmutableArray<ulong> IBotCredentials.OwnerIds => throw new NotImplementedException();
|
||||
|
||||
[JsonIgnore] RestartConfig IBotCredentials.RestartCommand => throw new NotImplementedException();
|
||||
|
||||
public bool IsOwner(IUser u)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOwner(IUser u) => OwnerIds.Contains(u.Id);
|
||||
}
|
||||
}
|
165
NadekoBot.Core/Services/Impl/CurrencyService.cs
Normal file
165
NadekoBot.Core/Services/Impl/CurrencyService.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Core.Services.Database;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Modules.Gambling.Services;
|
||||
|
||||
namespace NadekoBot.Core.Services
|
||||
{
|
||||
public class CurrencyService : ICurrencyService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly GamblingConfigService _gss;
|
||||
private readonly IUser _bot;
|
||||
|
||||
public CurrencyService(DbService db, DiscordSocketClient c, GamblingConfigService gss)
|
||||
{
|
||||
_db = db;
|
||||
_gss = gss;
|
||||
_bot = c.CurrentUser;
|
||||
}
|
||||
|
||||
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount) =>
|
||||
new CurrencyTransaction
|
||||
{
|
||||
Amount = amount,
|
||||
UserId = userId,
|
||||
Reason = reason ?? "-",
|
||||
};
|
||||
|
||||
private bool InternalChange(ulong userId, string userName, string discrim, string avatar,
|
||||
string reason, long amount, bool gamble, IUnitOfWork uow)
|
||||
{
|
||||
var result = uow.DiscordUsers.TryUpdateCurrencyState(userId, userName, discrim, avatar, amount);
|
||||
if (result)
|
||||
{
|
||||
var t = GetCurrencyTransaction(userId, reason, amount);
|
||||
uow._context.CurrencyTransactions.Add(t);
|
||||
|
||||
if (gamble)
|
||||
{
|
||||
var t2 = GetCurrencyTransaction(_bot.Id, reason, -amount);
|
||||
uow._context.CurrencyTransactions.Add(t2);
|
||||
uow.DiscordUsers.TryUpdateCurrencyState(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId, -amount, true);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task InternalAddAsync(ulong userId, string userName, string discrim, string avatar, string reason, long amount, bool gamble)
|
||||
{
|
||||
if (amount < 0)
|
||||
{
|
||||
throw new ArgumentException("You can't add negative amounts. Use RemoveAsync method for that.", nameof(amount));
|
||||
}
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
InternalChange(userId, userName, discrim, avatar, reason, amount, gamble, uow);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddAsync(ulong userId, string reason, long amount, bool gamble = false)
|
||||
{
|
||||
return InternalAddAsync(userId, null, null, null, reason, amount, gamble);
|
||||
}
|
||||
|
||||
public async Task AddAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
|
||||
{
|
||||
await InternalAddAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
|
||||
if (sendMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sign = _gss.Data.Currency.Sign;
|
||||
await (await user.GetOrCreateDMChannelAsync())
|
||||
.EmbedAsync(new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle($"Received Currency")
|
||||
.AddField("Amount", amount + _gss.Data.Currency.Sign)
|
||||
.AddField("Reason", reason));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false)
|
||||
{
|
||||
ulong[] idArray = userIds as ulong[] ?? userIds.ToArray();
|
||||
string[] reasonArray = reasons as string[] ?? reasons.ToArray();
|
||||
long[] amountArray = amounts as long[] ?? amounts.ToArray();
|
||||
|
||||
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
|
||||
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
|
||||
|
||||
var userIdHashSet = new HashSet<ulong>(idArray.Length);
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
for (int i = 0; i < idArray.Length; i++)
|
||||
{
|
||||
// i have to prevent same user changing more than once as it will cause db error
|
||||
if (userIdHashSet.Add(idArray[i]))
|
||||
InternalChange(idArray[i], null, null, null, reasonArray[i], amountArray[i], gamble, uow);
|
||||
}
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false)
|
||||
{
|
||||
var idArray = userIds as ulong[] ?? userIds.ToArray();
|
||||
var reasonArray = reasons as string[] ?? reasons.ToArray();
|
||||
var amountArray = amounts as long[] ?? amounts.ToArray();
|
||||
|
||||
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
|
||||
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
|
||||
|
||||
var userIdHashSet = new HashSet<ulong>(idArray.Length);
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
for (int i = 0; i < idArray.Length; i++)
|
||||
{
|
||||
// i have to prevent same user changing more than once as it will cause db error
|
||||
if (userIdHashSet.Add(idArray[i]))
|
||||
InternalChange(idArray[i], null, null, null, reasonArray[i], -amountArray[i], gamble, uow);
|
||||
}
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> InternalRemoveAsync(ulong userId, string userName, string userDiscrim, string avatar, string reason, long amount, bool gamble = false)
|
||||
{
|
||||
if (amount < 0)
|
||||
{
|
||||
throw new ArgumentException("You can't remove negative amounts. Use AddAsync method for that.", nameof(amount));
|
||||
}
|
||||
|
||||
bool result;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
result = InternalChange(userId, userName, userDiscrim, avatar, reason, -amount, gamble, uow);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false)
|
||||
{
|
||||
return InternalRemoveAsync(userId, null, null, null, reason, amount, gamble);
|
||||
}
|
||||
|
||||
public Task<bool> RemoveAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
|
||||
{
|
||||
return InternalRemoveAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
|
||||
}
|
||||
}
|
||||
}
|
63
NadekoBot.Core/Services/Impl/FontProvider.cs
Normal file
63
NadekoBot.Core/Services/Impl/FontProvider.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using SixLabors.Fonts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class FontProvider : INService
|
||||
{
|
||||
private readonly FontCollection _fonts;
|
||||
|
||||
public FontProvider()
|
||||
{
|
||||
_fonts = new FontCollection();
|
||||
|
||||
NotoSans = _fonts.Install("data/fonts/NotoSans-Bold.ttf");
|
||||
UniSans = _fonts.Install("data/fonts/Uni Sans.ttf");
|
||||
|
||||
FallBackFonts = new List<FontFamily>();
|
||||
|
||||
//FallBackFonts.Add(_fonts.Install("data/fonts/OpenSansEmoji.ttf"));
|
||||
|
||||
// try loading some emoji and jap fonts on windows as fallback fonts
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
try
|
||||
{
|
||||
string fontsfolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Fonts);
|
||||
FallBackFonts.Add(_fonts.Install(Path.Combine(fontsfolder, "seguiemj.ttf")));
|
||||
FallBackFonts.AddRange(_fonts.InstallCollection(Path.Combine(fontsfolder, "msgothic.ttc")));
|
||||
FallBackFonts.AddRange(_fonts.InstallCollection(Path.Combine(fontsfolder, "segoe.ttc")));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// any fonts present in data/fonts should be added as fallback fonts
|
||||
// this will allow support for special characters when drawing text
|
||||
foreach (var font in Directory.GetFiles(@"data/fonts"))
|
||||
{
|
||||
if (font.EndsWith(".ttf"))
|
||||
{
|
||||
FallBackFonts.Add(_fonts.Install(font));
|
||||
}
|
||||
else if (font.EndsWith(".ttc"))
|
||||
{
|
||||
FallBackFonts.AddRange(_fonts.InstallCollection(font));
|
||||
}
|
||||
}
|
||||
|
||||
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
|
||||
}
|
||||
|
||||
public FontFamily UniSans { get; }
|
||||
public FontFamily NotoSans { get; }
|
||||
//public FontFamily Emojis { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Font used for .rip command
|
||||
/// </summary>
|
||||
public Font RipFont { get; }
|
||||
public List<FontFamily> FallBackFonts { get; }
|
||||
}
|
||||
}
|
392
NadekoBot.Core/Services/Impl/GoogleApiService.cs
Normal file
392
NadekoBot.Core/Services/Impl/GoogleApiService.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
using Google;
|
||||
using Google.Apis.Customsearch.v1;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Urlshortener.v1;
|
||||
using Google.Apis.Urlshortener.v1.Data;
|
||||
using Google.Apis.YouTube.v3;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class GoogleApiService : IGoogleApiService
|
||||
{
|
||||
private const string SearchEngineId = "018084019232060951019:hs5piey28-e";
|
||||
|
||||
private YouTubeService yt;
|
||||
private UrlshortenerService sh;
|
||||
private CustomsearchService cs;
|
||||
|
||||
public GoogleApiService(IBotCredentials creds, IHttpClientFactory factory)
|
||||
{
|
||||
_creds = creds;
|
||||
_httpFactory = factory;
|
||||
|
||||
var bcs = new BaseClientService.Initializer
|
||||
{
|
||||
ApplicationName = "Nadeko Bot",
|
||||
ApiKey = _creds.GoogleApiKey,
|
||||
};
|
||||
|
||||
yt = new YouTubeService(bcs);
|
||||
sh = new UrlshortenerService(bcs);
|
||||
cs = new CustomsearchService(bcs);
|
||||
}
|
||||
private static readonly Regex plRegex = new Regex("(?:youtu\\.be\\/|list=)(?<id>[\\da-zA-Z\\-_]*)", RegexOptions.Compiled);
|
||||
public async Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(keywords))
|
||||
throw new ArgumentNullException(nameof(keywords));
|
||||
|
||||
if (count <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
var match = plRegex.Match(keywords);
|
||||
if (match.Length > 1)
|
||||
{
|
||||
return new[] { match.Groups["id"].Value.ToString() };
|
||||
}
|
||||
var query = yt.Search.List("snippet");
|
||||
query.MaxResults = count;
|
||||
query.Type = "playlist";
|
||||
query.Q = keywords;
|
||||
|
||||
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => i.Id.PlaylistId);
|
||||
}
|
||||
|
||||
//private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled);
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
// todo future add quota users
|
||||
public async Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
|
||||
if (count <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
var query = yt.Search.List("snippet");
|
||||
query.MaxResults = count;
|
||||
query.RelatedToVideoId = id;
|
||||
query.Type = "video";
|
||||
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(keywords))
|
||||
throw new ArgumentNullException(nameof(keywords));
|
||||
|
||||
if (count <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
var query = yt.Search.List("snippet");
|
||||
query.MaxResults = count;
|
||||
query.Q = keywords;
|
||||
query.Type = "video";
|
||||
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
|
||||
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(keywords))
|
||||
throw new ArgumentNullException(nameof(keywords));
|
||||
|
||||
if (count <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
var query = yt.Search.List("snippet");
|
||||
query.MaxResults = count;
|
||||
query.Q = keywords;
|
||||
query.Type = "video";
|
||||
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "http://www.youtube.com/watch?v=" + i.Id.VideoId));
|
||||
}
|
||||
|
||||
public Task<string> ShortenUrl(Uri url) => ShortenUrl(url.ToString());
|
||||
|
||||
public async Task<string> ShortenUrl(string url)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_creds.GoogleApiKey))
|
||||
return url;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await sh.Url.Insert(new Url { LongUrl = url }).ExecuteAsync().ConfigureAwait(false);
|
||||
return response.Id;
|
||||
}
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error shortening URL");
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(playlistId))
|
||||
throw new ArgumentNullException(nameof(playlistId));
|
||||
|
||||
if (count <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
string nextPageToken = null;
|
||||
|
||||
List<string> toReturn = new List<string>(count);
|
||||
|
||||
do
|
||||
{
|
||||
var toGet = count > 50 ? 50 : count;
|
||||
count -= toGet;
|
||||
|
||||
var query = yt.PlaylistItems.List("contentDetails");
|
||||
query.MaxResults = toGet;
|
||||
query.PlaylistId = playlistId;
|
||||
query.PageToken = nextPageToken;
|
||||
|
||||
var data = await query.ExecuteAsync().ConfigureAwait(false);
|
||||
|
||||
toReturn.AddRange(data.Items.Select(i => i.ContentDetails.VideoId));
|
||||
nextPageToken = data.NextPageToken;
|
||||
}
|
||||
while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken));
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds)
|
||||
{
|
||||
await Task.Yield();
|
||||
var videoIdsList = videoIds as List<string> ?? videoIds.ToList();
|
||||
|
||||
Dictionary<string, TimeSpan> toReturn = new Dictionary<string, TimeSpan>();
|
||||
|
||||
if (!videoIdsList.Any())
|
||||
return toReturn;
|
||||
var remaining = videoIdsList.Count;
|
||||
|
||||
do
|
||||
{
|
||||
var toGet = remaining > 50 ? 50 : remaining;
|
||||
remaining -= toGet;
|
||||
|
||||
var q = yt.Videos.List("contentDetails");
|
||||
q.Id = string.Join(",", videoIdsList.Take(toGet));
|
||||
videoIdsList = videoIdsList.Skip(toGet).ToList();
|
||||
var items = (await q.ExecuteAsync().ConfigureAwait(false)).Items;
|
||||
foreach (var i in items)
|
||||
{
|
||||
toReturn.Add(i.Id, System.Xml.XmlConvert.ToTimeSpan(i.ContentDetails.Duration));
|
||||
}
|
||||
}
|
||||
while (remaining > 0);
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public async Task<ImageResult> GetImageAsync(string query)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
var req = cs.Cse.List();
|
||||
req.Q = query;
|
||||
req.Cx = SearchEngineId;
|
||||
req.Num = 1;
|
||||
req.Fields = "items(image(contextLink,thumbnailLink),link)";
|
||||
req.SearchType = CseResource.ListRequest.SearchTypeEnum.Image;
|
||||
req.Start = new NadekoRandom().Next(0, 20);
|
||||
|
||||
var search = await req.ExecuteAsync().ConfigureAwait(false);
|
||||
|
||||
return new ImageResult(search.Items[0].Image, search.Items[0].Link);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Languages => _languageDictionary.Keys.OrderBy(x => x);
|
||||
private readonly Dictionary<string, string> _languageDictionary = new Dictionary<string, string>() {
|
||||
{ "afrikaans", "af"},
|
||||
{ "albanian", "sq"},
|
||||
{ "arabic", "ar"},
|
||||
{ "armenian", "hy"},
|
||||
{ "azerbaijani", "az"},
|
||||
{ "basque", "eu"},
|
||||
{ "belarusian", "be"},
|
||||
{ "bengali", "bn"},
|
||||
{ "bulgarian", "bg"},
|
||||
{ "catalan", "ca"},
|
||||
{ "chinese-traditional", "zh-TW"},
|
||||
{ "chinese-simplified", "zh-CN"},
|
||||
{ "chinese", "zh-CN"},
|
||||
{ "croatian", "hr"},
|
||||
{ "czech", "cs"},
|
||||
{ "danish", "da"},
|
||||
{ "dutch", "nl"},
|
||||
{ "english", "en"},
|
||||
{ "esperanto", "eo"},
|
||||
{ "estonian", "et"},
|
||||
{ "filipino", "tl"},
|
||||
{ "finnish", "fi"},
|
||||
{ "french", "fr"},
|
||||
{ "galician", "gl"},
|
||||
{ "german", "de"},
|
||||
{ "georgian", "ka"},
|
||||
{ "greek", "el"},
|
||||
{ "haitian Creole", "ht"},
|
||||
{ "hebrew", "iw"},
|
||||
{ "hindi", "hi"},
|
||||
{ "hungarian", "hu"},
|
||||
{ "icelandic", "is"},
|
||||
{ "indonesian", "id"},
|
||||
{ "irish", "ga"},
|
||||
{ "italian", "it"},
|
||||
{ "japanese", "ja"},
|
||||
{ "korean", "ko"},
|
||||
{ "lao", "lo"},
|
||||
{ "latin", "la"},
|
||||
{ "latvian", "lv"},
|
||||
{ "lithuanian", "lt"},
|
||||
{ "macedonian", "mk"},
|
||||
{ "malay", "ms"},
|
||||
{ "maltese", "mt"},
|
||||
{ "norwegian", "no"},
|
||||
{ "persian", "fa"},
|
||||
{ "polish", "pl"},
|
||||
{ "portuguese", "pt"},
|
||||
{ "romanian", "ro"},
|
||||
{ "russian", "ru"},
|
||||
{ "serbian", "sr"},
|
||||
{ "slovak", "sk"},
|
||||
{ "slovenian", "sl"},
|
||||
{ "spanish", "es"},
|
||||
{ "swahili", "sw"},
|
||||
{ "swedish", "sv"},
|
||||
{ "tamil", "ta"},
|
||||
{ "telugu", "te"},
|
||||
{ "thai", "th"},
|
||||
{ "turkish", "tr"},
|
||||
{ "ukrainian", "uk"},
|
||||
{ "urdu", "ur"},
|
||||
{ "vietnamese", "vi"},
|
||||
{ "welsh", "cy"},
|
||||
{ "yiddish", "yi"},
|
||||
|
||||
{ "af", "af"},
|
||||
{ "sq", "sq"},
|
||||
{ "ar", "ar"},
|
||||
{ "hy", "hy"},
|
||||
{ "az", "az"},
|
||||
{ "eu", "eu"},
|
||||
{ "be", "be"},
|
||||
{ "bn", "bn"},
|
||||
{ "bg", "bg"},
|
||||
{ "ca", "ca"},
|
||||
{ "zh-tw", "zh-TW"},
|
||||
{ "zh-cn", "zh-CN"},
|
||||
{ "hr", "hr"},
|
||||
{ "cs", "cs"},
|
||||
{ "da", "da"},
|
||||
{ "nl", "nl"},
|
||||
{ "en", "en"},
|
||||
{ "eo", "eo"},
|
||||
{ "et", "et"},
|
||||
{ "tl", "tl"},
|
||||
{ "fi", "fi"},
|
||||
{ "fr", "fr"},
|
||||
{ "gl", "gl"},
|
||||
{ "de", "de"},
|
||||
{ "ka", "ka"},
|
||||
{ "el", "el"},
|
||||
{ "ht", "ht"},
|
||||
{ "iw", "iw"},
|
||||
{ "hi", "hi"},
|
||||
{ "hu", "hu"},
|
||||
{ "is", "is"},
|
||||
{ "id", "id"},
|
||||
{ "ga", "ga"},
|
||||
{ "it", "it"},
|
||||
{ "ja", "ja"},
|
||||
{ "ko", "ko"},
|
||||
{ "lo", "lo"},
|
||||
{ "la", "la"},
|
||||
{ "lv", "lv"},
|
||||
{ "lt", "lt"},
|
||||
{ "mk", "mk"},
|
||||
{ "ms", "ms"},
|
||||
{ "mt", "mt"},
|
||||
{ "no", "no"},
|
||||
{ "fa", "fa"},
|
||||
{ "pl", "pl"},
|
||||
{ "pt", "pt"},
|
||||
{ "ro", "ro"},
|
||||
{ "ru", "ru"},
|
||||
{ "sr", "sr"},
|
||||
{ "sk", "sk"},
|
||||
{ "sl", "sl"},
|
||||
{ "es", "es"},
|
||||
{ "sw", "sw"},
|
||||
{ "sv", "sv"},
|
||||
{ "ta", "ta"},
|
||||
{ "te", "te"},
|
||||
{ "th", "th"},
|
||||
{ "tr", "tr"},
|
||||
{ "uk", "uk"},
|
||||
{ "ur", "ur"},
|
||||
{ "vi", "vi"},
|
||||
{ "cy", "cy"},
|
||||
{ "yi", "yi"},
|
||||
};
|
||||
|
||||
public async Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage)
|
||||
{
|
||||
await Task.Yield();
|
||||
string text;
|
||||
|
||||
if (!_languageDictionary.ContainsKey(sourceLanguage) ||
|
||||
!_languageDictionary.ContainsKey(targetLanguage))
|
||||
throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage));
|
||||
|
||||
|
||||
var url = new Uri(string.Format("https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}",
|
||||
ConvertToLanguageCode(sourceLanguage),
|
||||
ConvertToLanguageCode(targetLanguage),
|
||||
WebUtility.UrlEncode(sourceText)));
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36");
|
||||
text = await http.GetStringAsync(url).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return (string.Concat(JArray.Parse(text)[0].Select(x => x[0])));
|
||||
}
|
||||
|
||||
private string ConvertToLanguageCode(string language)
|
||||
{
|
||||
_languageDictionary.TryGetValue(language, out var mode);
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
}
|
274
NadekoBot.Core/Services/Impl/ImagesService.cs
Normal file
274
NadekoBot.Core/Services/Impl/ImagesService.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.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 Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public sealed class RedisImagesCache : IImageCache
|
||||
{
|
||||
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 _oldBasePath = "data/images/";
|
||||
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 RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
|
||||
{
|
||||
_con = con;
|
||||
_creds = creds;
|
||||
_http = new HttpClient();
|
||||
|
||||
Migrate();
|
||||
ImageUrls = JsonConvert.DeserializeObject<ImageUrls>(
|
||||
File.ReadAllText(Path.Combine(_basePath, "images.json")));
|
||||
}
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
try
|
||||
{
|
||||
Migrate1();
|
||||
Migrate2();
|
||||
Migrate3();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex.Message);
|
||||
Log.Error("Something has been incorrectly formatted in your 'images.json' file.\n" +
|
||||
"Use the 'images_example.json' file as reference to fix it and restart the bot.");
|
||||
}
|
||||
}
|
||||
|
||||
private void Migrate1()
|
||||
{
|
||||
if (!File.Exists(Path.Combine(_oldBasePath, "images.json")))
|
||||
return;
|
||||
Log.Information("Migrating images v0 to images v1.");
|
||||
// load old images
|
||||
var oldUrls = JsonConvert.DeserializeObject<ImageUrls>(
|
||||
File.ReadAllText(Path.Combine(_oldBasePath, "images.json")));
|
||||
// load new images
|
||||
var newUrls = JsonConvert.DeserializeObject<ImageUrls>(
|
||||
File.ReadAllText(Path.Combine(_basePath, "images.json")));
|
||||
|
||||
//swap new links with old ones if set. Also update old links.
|
||||
newUrls.Coins = oldUrls.Coins;
|
||||
|
||||
newUrls.Currency = oldUrls.Currency;
|
||||
newUrls.Dice = oldUrls.Dice;
|
||||
newUrls.Rategirl = oldUrls.Rategirl;
|
||||
newUrls.Xp = oldUrls.Xp;
|
||||
newUrls.Version = 1;
|
||||
|
||||
File.WriteAllText(Path.Combine(_basePath, "images.json"),
|
||||
JsonConvert.SerializeObject(newUrls, Formatting.Indented));
|
||||
File.Delete(Path.Combine(_oldBasePath, "images.json"));
|
||||
}
|
||||
|
||||
private void Migrate2()
|
||||
{
|
||||
// load new images
|
||||
var urls = JsonConvert.DeserializeObject<ImageUrls>(File.ReadAllText(Path.Combine(_basePath, "images.json")));
|
||||
|
||||
if (urls.Version >= 2)
|
||||
return;
|
||||
Log.Information("Migrating images v1 to images v2.");
|
||||
urls.Version = 2;
|
||||
|
||||
var prefix = $"{_creds.RedisKey()}_localimg_";
|
||||
_db.KeyDelete(new[] {
|
||||
prefix + "heads",
|
||||
prefix + "tails",
|
||||
prefix + "dice",
|
||||
prefix + "slot_background",
|
||||
prefix + "slotnumbers",
|
||||
prefix + "slotemojis",
|
||||
prefix + "wife_matrix",
|
||||
prefix + "rategirl_dot",
|
||||
prefix + "xp_card",
|
||||
prefix + "rip",
|
||||
prefix + "rip_overlay" }
|
||||
.Select(x => (RedisKey)x).ToArray());
|
||||
|
||||
File.WriteAllText(Path.Combine(_basePath, "images.json"), JsonConvert.SerializeObject(urls, Formatting.Indented));
|
||||
}
|
||||
|
||||
private void Migrate3()
|
||||
{
|
||||
var urls = JsonConvert.DeserializeObject<ImageUrls>(
|
||||
File.ReadAllText(Path.Combine(_basePath, "images.json")));
|
||||
|
||||
if (urls.Version >= 3)
|
||||
return;
|
||||
urls.Version = 3;
|
||||
Log.Information("Migrating images v2 to images v3.");
|
||||
|
||||
var baseStr = "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/";
|
||||
|
||||
var replacementTable = new Dictionary<Uri, Uri>()
|
||||
{
|
||||
{new Uri(baseStr + "0.jpg"), new Uri(baseStr + "0.png") },
|
||||
{new Uri(baseStr + "1.jpg"), new Uri(baseStr + "1.png") },
|
||||
{new Uri(baseStr + "2.jpg"), new Uri(baseStr + "2.png") }
|
||||
};
|
||||
|
||||
if (replacementTable.Keys.Any(x => urls.Currency.Contains(x)))
|
||||
{
|
||||
urls.Currency = urls.Currency.Select(x => replacementTable.TryGetValue(x, out var newUri)
|
||||
? newUri
|
||||
: x).Append(new Uri(baseStr + "3.png"))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
File.WriteAllText(Path.Combine(_basePath, "images.json"), JsonConvert.SerializeObject(urls, Formatting.Indented));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
120
NadekoBot.Core/Services/Impl/Localization.cs
Normal file
120
NadekoBot.Core/Services/Impl/Localization.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Discord;
|
||||
using NadekoBot.Common;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class Localization : ILocalization
|
||||
{
|
||||
private readonly BotConfigService _bss;
|
||||
private readonly DbService _db;
|
||||
|
||||
public ConcurrentDictionary<ulong, CultureInfo> GuildCultureInfos { get; }
|
||||
public CultureInfo DefaultCultureInfo => _bss.Data.DefaultLocale;
|
||||
|
||||
private static readonly Dictionary<string, CommandData> _commandData = JsonConvert.DeserializeObject<Dictionary<string, CommandData>>(
|
||||
File.ReadAllText("./data/strings/commands/commands.en-US.json"));
|
||||
|
||||
public Localization(BotConfigService bss, NadekoBot bot, DbService db)
|
||||
{
|
||||
_bss = bss;
|
||||
_db = db;
|
||||
|
||||
var cultureInfoNames = bot.AllGuildConfigs
|
||||
.ToDictionary(x => x.GuildId, x => x.Locale);
|
||||
|
||||
GuildCultureInfos = new ConcurrentDictionary<ulong, CultureInfo>(cultureInfoNames.ToDictionary(x => x.Key, x =>
|
||||
{
|
||||
CultureInfo cultureInfo = null;
|
||||
try
|
||||
{
|
||||
if (x.Value == null)
|
||||
return null;
|
||||
cultureInfo = new CultureInfo(x.Value);
|
||||
}
|
||||
catch { }
|
||||
return cultureInfo;
|
||||
}).Where(x => x.Value != null));
|
||||
}
|
||||
|
||||
public void SetGuildCulture(IGuild guild, CultureInfo ci) =>
|
||||
SetGuildCulture(guild.Id, ci);
|
||||
|
||||
public void SetGuildCulture(ulong guildId, CultureInfo ci)
|
||||
{
|
||||
if (ci.Name == _bss.Data.DefaultLocale.Name)
|
||||
{
|
||||
RemoveGuildCulture(guildId);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var gc = uow.GuildConfigs.ForId(guildId, set => set);
|
||||
gc.Locale = ci.Name;
|
||||
uow.SaveChanges();
|
||||
}
|
||||
|
||||
GuildCultureInfos.AddOrUpdate(guildId, ci, (id, old) => ci);
|
||||
}
|
||||
|
||||
public void RemoveGuildCulture(IGuild guild) =>
|
||||
RemoveGuildCulture(guild.Id);
|
||||
|
||||
public void RemoveGuildCulture(ulong guildId)
|
||||
{
|
||||
|
||||
if (GuildCultureInfos.TryRemove(guildId, out var _))
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var gc = uow.GuildConfigs.ForId(guildId, set => set);
|
||||
gc.Locale = null;
|
||||
uow.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDefaultCulture(CultureInfo ci)
|
||||
{
|
||||
_bss.ModifyConfig(bs =>
|
||||
{
|
||||
bs.DefaultLocale = ci;
|
||||
});
|
||||
}
|
||||
|
||||
public void ResetDefaultCulture() =>
|
||||
SetDefaultCulture(CultureInfo.CurrentCulture);
|
||||
|
||||
public CultureInfo GetCultureInfo(IGuild guild) =>
|
||||
GetCultureInfo(guild?.Id);
|
||||
|
||||
public CultureInfo GetCultureInfo(ulong? guildId)
|
||||
{
|
||||
if (guildId is null || !GuildCultureInfos.TryGetValue(guildId.Value, out var info) || info is null)
|
||||
return _bss.Data.DefaultLocale;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public static CommandData LoadCommand(string key)
|
||||
{
|
||||
_commandData.TryGetValue(key, out var toReturn);
|
||||
|
||||
if (toReturn == null)
|
||||
return new CommandData
|
||||
{
|
||||
Cmd = key,
|
||||
Desc = key,
|
||||
Usage = new[] { key },
|
||||
};
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
}
|
215
NadekoBot.Core/Services/Impl/RedisCache.cs
Normal file
215
NadekoBot.Core/Services/Impl/RedisCache.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class RedisCache : IDataCache
|
||||
{
|
||||
public ConnectionMultiplexer Redis { get; }
|
||||
|
||||
public IImageCache LocalImages { get; }
|
||||
public ILocalDataCache LocalData { get; }
|
||||
|
||||
private readonly string _redisKey;
|
||||
private readonly EndPoint _redisEndpoint;
|
||||
|
||||
public RedisCache(IBotCredentials creds, int shardId)
|
||||
{
|
||||
var conf = ConfigurationOptions.Parse(creds.RedisOptions);
|
||||
|
||||
Redis = ConnectionMultiplexer.Connect(conf);
|
||||
_redisEndpoint = Redis.GetEndPoints().First();
|
||||
Redis.PreserveAsyncOrder = false;
|
||||
LocalImages = new RedisImagesCache(Redis, creds);
|
||||
LocalData = new RedisLocalDataCache(Redis, creds, shardId);
|
||||
_redisKey = creds.RedisKey();
|
||||
}
|
||||
|
||||
// things here so far don't need the bot id
|
||||
// because it's a good thing if different bots
|
||||
// which are hosted on the same PC
|
||||
// can re-use the same image/anime data
|
||||
public async Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
byte[] x = await _db.StringGetAsync("image_" + key).ConfigureAwait(false);
|
||||
return (x != null, x);
|
||||
}
|
||||
|
||||
public Task SetImageDataAsync(Uri key, byte[] data)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
return _db.StringSetAsync("image_" + key, data);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
string x = await _db.StringGetAsync("anime_" + key).ConfigureAwait(false);
|
||||
return (x != null, x);
|
||||
}
|
||||
|
||||
public Task SetAnimeDataAsync(string key, string data)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
return _db.StringSetAsync("anime_" + key, data, expiry: TimeSpan.FromHours(3));
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Data)> TryGetNovelDataAsync(string key)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
string x = await _db.StringGetAsync("novel_" + key).ConfigureAwait(false);
|
||||
return (x != null, x);
|
||||
}
|
||||
|
||||
public Task SetNovelDataAsync(string key, string data)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
return _db.StringSetAsync("novel_" + key, data, expiry: TimeSpan.FromHours(3));
|
||||
}
|
||||
|
||||
private readonly object timelyLock = new object();
|
||||
public TimeSpan? AddTimelyClaim(ulong id, int period)
|
||||
{
|
||||
if (period == 0)
|
||||
return null;
|
||||
lock (timelyLock)
|
||||
{
|
||||
var time = TimeSpan.FromHours(period);
|
||||
var _db = Redis.GetDatabase();
|
||||
if ((bool?)_db.StringGet($"{_redisKey}_timelyclaim_{id}") == null)
|
||||
{
|
||||
_db.StringSet($"{_redisKey}_timelyclaim_{id}", true, time);
|
||||
return null;
|
||||
}
|
||||
return _db.KeyTimeToLive($"{_redisKey}_timelyclaim_{id}");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAllTimelyClaims()
|
||||
{
|
||||
var server = Redis.GetServer(_redisEndpoint);
|
||||
var _db = Redis.GetDatabase();
|
||||
foreach (var k in server.Keys(pattern: $"{_redisKey}_timelyclaim_*"))
|
||||
{
|
||||
_db.KeyDelete(k, CommandFlags.FireAndForget);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
time = _db.KeyTimeToLive($"{_redisKey}_affinity_{userId}");
|
||||
if (time == null)
|
||||
{
|
||||
time = TimeSpan.FromMinutes(30);
|
||||
_db.StringSet($"{_redisKey}_affinity_{userId}", true, time);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
time = _db.KeyTimeToLive($"{_redisKey}_divorce_{userId}");
|
||||
if (time == null)
|
||||
{
|
||||
time = TimeSpan.FromHours(6);
|
||||
_db.StringSet($"{_redisKey}_divorce_{userId}", true, time);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task SetStreamDataAsync(string url, string data)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
return _db.StringSetAsync($"{_redisKey}_stream_{url}", data, expiry: TimeSpan.FromHours(6));
|
||||
}
|
||||
|
||||
public bool TryGetStreamData(string url, out string dataStr)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
dataStr = _db.StringGet($"{_redisKey}_stream_{url}");
|
||||
|
||||
return !string.IsNullOrWhiteSpace(dataStr);
|
||||
}
|
||||
|
||||
public TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
if (_db.StringSet($"{_redisKey}_ratelimit_{id}_{name}",
|
||||
0, // i don't use the value
|
||||
TimeSpan.FromSeconds(expireIn),
|
||||
When.NotExists))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _db.KeyTimeToLive($"{_redisKey}_ratelimit_{id}_{name}");
|
||||
}
|
||||
|
||||
public bool TryGetEconomy(out string data)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
if ((data = _db.StringGet($"{_redisKey}_economy")) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetEconomy(string data)
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
_db.StringSet($"{_redisKey}_economy",
|
||||
data,
|
||||
expiry: TimeSpan.FromMinutes(3));
|
||||
}
|
||||
|
||||
public async Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(string key, Func<TParam, Task<TOut>> factory, TParam param, TimeSpan expiry) where TOut : class
|
||||
{
|
||||
var _db = Redis.GetDatabase();
|
||||
|
||||
RedisValue data = await _db.StringGetAsync(key).ConfigureAwait(false);
|
||||
if (!data.HasValue)
|
||||
{
|
||||
var obj = await factory(param).ConfigureAwait(false);
|
||||
|
||||
if (obj == null)
|
||||
return default(TOut);
|
||||
|
||||
await _db.StringSetAsync(key, JsonConvert.SerializeObject(obj),
|
||||
expiry: expiry).ConfigureAwait(false);
|
||||
|
||||
return obj;
|
||||
}
|
||||
return (TOut)JsonConvert.DeserializeObject(data, typeof(TOut));
|
||||
}
|
||||
|
||||
public DateTime GetLastCurrencyDecay()
|
||||
{
|
||||
var db = Redis.GetDatabase();
|
||||
|
||||
var str = (string)db.StringGet($"{_redisKey}_last_currency_decay");
|
||||
if(string.IsNullOrEmpty(str))
|
||||
return DateTime.MinValue;
|
||||
|
||||
return JsonConvert.DeserializeObject<DateTime>(str);
|
||||
}
|
||||
|
||||
public void SetLastCurrencyDecay()
|
||||
{
|
||||
var db = Redis.GetDatabase();
|
||||
|
||||
db.StringSet($"{_redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow));
|
||||
}
|
||||
}
|
||||
}
|
123
NadekoBot.Core/Services/Impl/RedisLocalDataCache.cs
Normal file
123
NadekoBot.Core/Services/Impl/RedisLocalDataCache.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using NadekoBot.Core.Common.Pokemon;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Games.Common.Trivia;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class RedisLocalDataCache : ILocalDataCache
|
||||
{
|
||||
private readonly ConnectionMultiplexer _con;
|
||||
private readonly IBotCredentials _creds;
|
||||
|
||||
private IDatabase _db => _con.GetDatabase();
|
||||
|
||||
private const string pokemonAbilitiesFile = "data/pokemon/pokemon_abilities.json";
|
||||
private const string pokemonListFile = "data/pokemon/pokemon_list.json";
|
||||
private const string pokemonMapPath = "data/pokemon/name-id_map.json";
|
||||
private const string questionsFile = "data/trivia_questions.json";
|
||||
|
||||
public IReadOnlyDictionary<string, SearchPokemon> Pokemons
|
||||
{
|
||||
get
|
||||
{
|
||||
return Get<Dictionary<string, SearchPokemon>>("pokemon_list");
|
||||
}
|
||||
private set
|
||||
{
|
||||
Set("pokemon_list", value);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
|
||||
{
|
||||
get
|
||||
{
|
||||
return Get<Dictionary<string, SearchPokemonAbility>>("pokemon_abilities");
|
||||
}
|
||||
private set
|
||||
{
|
||||
Set("pokemon_abilities", value);
|
||||
}
|
||||
}
|
||||
|
||||
public TriviaQuestion[] TriviaQuestions
|
||||
{
|
||||
get
|
||||
{
|
||||
return Get<TriviaQuestion[]>("trivia_questions");
|
||||
}
|
||||
private set
|
||||
{
|
||||
Set("trivia_questions", value);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<int, string> PokemonMap
|
||||
{
|
||||
get
|
||||
{
|
||||
return Get<Dictionary<int, string>>("pokemon_map");
|
||||
}
|
||||
private set
|
||||
{
|
||||
Set("pokemon_map", value);
|
||||
}
|
||||
}
|
||||
|
||||
public RedisLocalDataCache(ConnectionMultiplexer con, IBotCredentials creds, int shardId)
|
||||
{
|
||||
_con = con;
|
||||
_creds = creds;
|
||||
|
||||
if (shardId == 0)
|
||||
{
|
||||
if (!File.Exists(pokemonListFile))
|
||||
{
|
||||
Log.Warning($"{pokemonListFile} is missing. Pokemon abilities not loaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
Pokemons = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(pokemonListFile));
|
||||
}
|
||||
|
||||
if (!File.Exists(pokemonAbilitiesFile))
|
||||
{
|
||||
Log.Warning($"{pokemonAbilitiesFile} is missing. Pokemon abilities not loaded.");
|
||||
}
|
||||
else
|
||||
{
|
||||
PokemonAbilities = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(File.ReadAllText(pokemonAbilitiesFile));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
|
||||
PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
|
||||
.ToDictionary(x => x.Id, x => x.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error loading local data");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T Get<T>(string key) where T : class
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(_db.StringGet($"{_creds.RedisKey()}_localdata_{key}"));
|
||||
}
|
||||
|
||||
private void Set(string key, object obj)
|
||||
{
|
||||
_db.StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
|
||||
}
|
||||
}
|
||||
}
|
78
NadekoBot.Core/Services/Impl/SoundCloudApiService.cs
Normal file
78
NadekoBot.Core/Services/Impl/SoundCloudApiService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class SoundCloudApiService : INService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public SoundCloudApiService(IHttpClientFactory factory)
|
||||
{
|
||||
_httpFactory = factory;
|
||||
}
|
||||
|
||||
public async Task<SoundCloudVideo> ResolveVideoAsync(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
|
||||
string response = "";
|
||||
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
response = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={url}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo>(response);
|
||||
if (responseObj?.Kind != "track")
|
||||
throw new InvalidOperationException("Url is either not a track, or it doesn't exist.");
|
||||
|
||||
return responseObj;
|
||||
}
|
||||
|
||||
public async Task<SoundCloudVideo> GetVideoByQueryAsync(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
var response = "";
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
response = await http.GetStringAsync(new Uri($"https://scapi.nadeko.bot/tracks?q={Uri.EscapeDataString(query)}")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo[]>(response)
|
||||
.FirstOrDefault(s => s.Streamable is true);
|
||||
|
||||
if (responseObj?.Kind != "track")
|
||||
throw new InvalidOperationException("Query yielded no results.");
|
||||
|
||||
return responseObj;
|
||||
}
|
||||
}
|
||||
|
||||
public class SoundCloudVideo
|
||||
{
|
||||
public string Kind { get; set; } = "";
|
||||
public long Id { get; set; } = 0;
|
||||
public SoundCloudUser User { get; set; } = new SoundCloudUser();
|
||||
public string Title { get; set; } = "";
|
||||
public string FullName => User.Name + " - " + Title;
|
||||
public bool? Streamable { get; set; } = false;
|
||||
public int Duration { get; set; }
|
||||
[JsonProperty("permalink_url")]
|
||||
public string TrackLink { get; set; } = "";
|
||||
[JsonProperty("artwork_url")]
|
||||
public string ArtworkUrl { get; set; } = "";
|
||||
}
|
||||
|
||||
public class SoundCloudUser
|
||||
{
|
||||
[JsonProperty("username")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
24
NadekoBot.Core/Services/Impl/StartingGuildsListService.cs
Normal file
24
NadekoBot.Core/Services/Impl/StartingGuildsListService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Discord.WebSocket;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Collections;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class StartingGuildsService : IEnumerable<ulong>, INService
|
||||
{
|
||||
private readonly ImmutableList<ulong> _guilds;
|
||||
|
||||
public StartingGuildsService(DiscordSocketClient client)
|
||||
{
|
||||
this._guilds = client.Guilds.Select(x => x.Id).ToImmutableList();
|
||||
}
|
||||
|
||||
public IEnumerator<ulong> GetEnumerator() =>
|
||||
_guilds.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() =>
|
||||
_guilds.GetEnumerator();
|
||||
}
|
||||
}
|
175
NadekoBot.Core/Services/Impl/StatsService.cs
Normal file
175
NadekoBot.Core/Services/Impl/StatsService.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class StatsService : IStatsService
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly DateTime _started;
|
||||
|
||||
public const string BotVersion = "3.0.0-alpha1";
|
||||
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;
|
||||
public long TextChannels => Interlocked.Read(ref _textChannels);
|
||||
private long _voiceChannels;
|
||||
public long VoiceChannels => Interlocked.Read(ref _voiceChannels);
|
||||
private long _messageCounter;
|
||||
public long MessageCounter => Interlocked.Read(ref _messageCounter);
|
||||
private long _commandsRan;
|
||||
public long CommandsRan => Interlocked.Read(ref _commandsRan);
|
||||
|
||||
private readonly Timer _botlistTimer;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public StatsService(DiscordSocketClient client, CommandHandler cmdHandler,
|
||||
IBotCredentials creds, IHttpClientFactory factory)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_httpFactory = factory;
|
||||
|
||||
_started = DateTime.UtcNow;
|
||||
_client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref _messageCounter));
|
||||
cmdHandler.CommandExecuted += (_, e) => Task.FromResult(Interlocked.Increment(ref _commandsRan));
|
||||
|
||||
_client.ChannelCreated += (c) =>
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
if (c is ITextChannel)
|
||||
Interlocked.Increment(ref _textChannels);
|
||||
else if (c is IVoiceChannel)
|
||||
Interlocked.Increment(ref _voiceChannels);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.ChannelDestroyed += (c) =>
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
if (c is ITextChannel)
|
||||
Interlocked.Decrement(ref _textChannels);
|
||||
else if (c is IVoiceChannel)
|
||||
Interlocked.Decrement(ref _voiceChannels);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.GuildAvailable += (g) =>
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
var tc = g.Channels.Count(cx => cx is ITextChannel);
|
||||
var vc = g.Channels.Count - tc;
|
||||
Interlocked.Add(ref _textChannels, tc);
|
||||
Interlocked.Add(ref _voiceChannels, vc);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.JoinedGuild += (g) =>
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
var tc = g.Channels.Count(cx => cx is ITextChannel);
|
||||
var vc = g.Channels.Count - tc;
|
||||
Interlocked.Add(ref _textChannels, tc);
|
||||
Interlocked.Add(ref _voiceChannels, vc);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.GuildUnavailable += (g) =>
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
var tc = g.Channels.Count(cx => cx is ITextChannel);
|
||||
var vc = g.Channels.Count - tc;
|
||||
Interlocked.Add(ref _textChannels, -tc);
|
||||
Interlocked.Add(ref _voiceChannels, -vc);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.LeftGuild += (g) =>
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
var tc = g.Channels.Count(cx => cx is ITextChannel);
|
||||
var vc = g.Channels.Count - tc;
|
||||
Interlocked.Add(ref _textChannels, -tc);
|
||||
Interlocked.Add(ref _voiceChannels, -vc);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_botlistTimer = new Timer(async (state) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.BotListToken))
|
||||
return;
|
||||
try
|
||||
{
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
using (var content = new FormUrlEncodedContent(
|
||||
new Dictionary<string, string> {
|
||||
{ "shard_count", _creds.TotalShards.ToString()},
|
||||
{ "shard_id", client.ShardId.ToString() },
|
||||
{ "server_count", client.Guilds.Count().ToString() }
|
||||
}))
|
||||
{
|
||||
content.Headers.Clear();
|
||||
content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
|
||||
http.DefaultRequestHeaders.Add("Authorization", _creds.BotListToken);
|
||||
|
||||
using (await http.PostAsync(new Uri($"https://discordbots.org/api/bots/{client.CurrentUser.Id}/stats"), content).ConfigureAwait(false)) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error ");
|
||||
// ignored
|
||||
}
|
||||
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
var guilds = _client.Guilds.ToArray();
|
||||
_textChannels = guilds.Sum(g => g.Channels.Count(cx => cx is ITextChannel));
|
||||
_voiceChannels = guilds.Sum(g => g.Channels.Count(cx => cx is IVoiceChannel));
|
||||
}
|
||||
|
||||
public TimeSpan GetUptime() =>
|
||||
DateTime.UtcNow - _started;
|
||||
|
||||
public string GetUptimeString(string separator = ", ")
|
||||
{
|
||||
var time = GetUptime();
|
||||
return $"{time.Days} days{separator}{time.Hours} hours{separator}{time.Minutes} minutes";
|
||||
}
|
||||
}
|
||||
}
|
7
NadekoBot.Core/Services/Impl/SyncPreconditionService.cs
Normal file
7
NadekoBot.Core/Services/Impl/SyncPreconditionService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class SyncPreconditionService
|
||||
{
|
||||
|
||||
}
|
||||
}
|
81
NadekoBot.Core/Services/Impl/YtdlOperation.cs
Normal file
81
NadekoBot.Core/Services/Impl/YtdlOperation.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Extensions;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Services.Impl
|
||||
{
|
||||
public class YtdlOperation
|
||||
{
|
||||
private readonly string _baseArgString;
|
||||
|
||||
public YtdlOperation(string baseArgString)
|
||||
{
|
||||
_baseArgString = baseArgString;
|
||||
}
|
||||
|
||||
private Process CreateProcess(string[] args)
|
||||
{
|
||||
args = args.Map(arg => arg.Replace("\"", ""));
|
||||
return new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo()
|
||||
{
|
||||
FileName = "youtube-dl",
|
||||
Arguments = string.Format(_baseArgString, args),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
CreateNoWindow = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<string> GetDataAsync(params string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = CreateProcess(args);
|
||||
|
||||
Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
|
||||
process.Start();
|
||||
|
||||
var str = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||
var err = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(err))
|
||||
Log.Warning("YTDL warning: {YtdlWarning}", err);
|
||||
|
||||
return str;
|
||||
}
|
||||
catch (Win32Exception)
|
||||
{
|
||||
Log.Error("youtube-dl is likely not installed. " +
|
||||
"Please install it before running the command again");
|
||||
return default;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex , "Exception running youtube-dl: {ErrorMessage}", ex.Message);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<string> EnumerateDataAsync(params string[] args)
|
||||
{
|
||||
using var process = CreateProcess(args);
|
||||
|
||||
Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
|
||||
process.Start();
|
||||
|
||||
string line;
|
||||
while((line = await process.StandardOutput.ReadLineAsync()) != null)
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user