Killed history

This commit is contained in:
Kwoth
2021-09-06 21:29:22 +02:00
commit 7aca29ae8a
950 changed files with 366651 additions and 0 deletions

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Core.Services.Impl
{
public class SyncPreconditionService
{
}
}

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