Removed module projects because it can't work like that atm. Commented out package commands.
This commit is contained in:
@ -0,0 +1,74 @@
|
||||
using NadekoBot.Core.Services;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services
|
||||
{
|
||||
public class AnimeSearchService : INService
|
||||
{
|
||||
private readonly Logger _log;
|
||||
private readonly IDataCache _cache;
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public AnimeSearchService(IDataCache cache)
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
_cache = cache;
|
||||
_http = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<AnimeResult> GetAnimeData(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
try
|
||||
{
|
||||
|
||||
var link = "https://aniapi.nadekobot.me/anime/" + Uri.EscapeDataString(query.Replace("/", " "));
|
||||
link = link.ToLowerInvariant();
|
||||
var (ok, data) = await _cache.TryGetAnimeDataAsync(link).ConfigureAwait(false);
|
||||
if (!ok)
|
||||
{
|
||||
data = await _http.GetStringAsync(link).ConfigureAwait(false);
|
||||
await _cache.SetAnimeDataAsync(link, data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
return JsonConvert.DeserializeObject<AnimeResult>(data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MangaResult> GetMangaData(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
try
|
||||
{
|
||||
|
||||
var link = "https://aniapi.nadekobot.me/manga/" + Uri.EscapeDataString(query.Replace("/", " "));
|
||||
link = link.ToLowerInvariant();
|
||||
var (ok, data) = await _cache.TryGetAnimeDataAsync(link).ConfigureAwait(false);
|
||||
if (!ok)
|
||||
{
|
||||
data = await _http.GetStringAsync(link).ConfigureAwait(false);
|
||||
await _cache.SetAnimeDataAsync(link, data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
return JsonConvert.DeserializeObject<MangaResult>(data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
214
NadekoBot.Core/Modules/Searches/Services/FeedsService.cs
Normal file
214
NadekoBot.Core/Modules/Searches/Services/FeedsService.cs
Normal file
@ -0,0 +1,214 @@
|
||||
using Discord;
|
||||
using Microsoft.SyndicationFeed;
|
||||
using Microsoft.SyndicationFeed.Rss;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Collections.Generic;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Concurrent;
|
||||
using Discord.WebSocket;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services
|
||||
{
|
||||
public class FeedsService : INService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly ConcurrentDictionary<string, HashSet<FeedSub>> _subs;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ConcurrentDictionary<string, DateTime> _lastPosts =
|
||||
new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
public FeedsService(NadekoBot bot, DbService db, DiscordSocketClient client)
|
||||
{
|
||||
_db = db;
|
||||
|
||||
_subs = bot
|
||||
.AllGuildConfigs
|
||||
.SelectMany(x => x.FeedSubs)
|
||||
.GroupBy(x => x.Url)
|
||||
.ToDictionary(x => x.Key, x => x.ToHashSet())
|
||||
.ToConcurrent();
|
||||
|
||||
_client = client;
|
||||
|
||||
foreach (var kvp in _subs)
|
||||
{
|
||||
// to make sure rss feeds don't post right away, but
|
||||
// only the updates from after the bot has started
|
||||
_lastPosts.AddOrUpdate(kvp.Key, DateTime.UtcNow, (k, old) => DateTime.UtcNow);
|
||||
}
|
||||
|
||||
var _ = Task.Run(TrackFeeds);
|
||||
}
|
||||
|
||||
public async Task<EmbedBuilder> TrackFeeds()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
foreach (var kvp in _subs)
|
||||
{
|
||||
if (kvp.Value.Count == 0)
|
||||
continue;
|
||||
|
||||
if (!_lastPosts.TryGetValue(kvp.Key, out DateTime lastTime))
|
||||
lastTime = _lastPosts.AddOrUpdate(kvp.Key, DateTime.UtcNow, (k, old) => DateTime.UtcNow);
|
||||
|
||||
var rssUrl = kvp.Key;
|
||||
try
|
||||
{
|
||||
using (var xmlReader = XmlReader.Create(rssUrl, new XmlReaderSettings() { Async = true }))
|
||||
{
|
||||
var feedReader = new RssFeedReader(xmlReader);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithAuthor(kvp.Key)
|
||||
.WithOkColor();
|
||||
|
||||
while (await feedReader.Read() && feedReader.ElementType != SyndicationElementType.Item)
|
||||
{
|
||||
switch (feedReader.ElementType)
|
||||
{
|
||||
case SyndicationElementType.Link:
|
||||
var uri = await feedReader.ReadLink();
|
||||
embed.WithAuthor(kvp.Key, url: uri.Uri.AbsoluteUri);
|
||||
break;
|
||||
case SyndicationElementType.Content:
|
||||
var content = await feedReader.ReadContent();
|
||||
break;
|
||||
case SyndicationElementType.Category:
|
||||
break;
|
||||
case SyndicationElementType.Image:
|
||||
ISyndicationImage image = await feedReader.ReadImage();
|
||||
embed.WithThumbnailUrl(image.Url.AbsoluteUri);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ISyndicationItem item = await feedReader.ReadItem();
|
||||
if (item.Published.UtcDateTime <= lastTime)
|
||||
continue;
|
||||
|
||||
var desc = item.Description.StripHTML();
|
||||
|
||||
lastTime = item.Published.UtcDateTime;
|
||||
var title = string.IsNullOrWhiteSpace(item.Title) ? "-" : item.Title;
|
||||
desc = Format.Code(item.Published.ToString()) + Environment.NewLine + desc;
|
||||
var link = item.Links.FirstOrDefault();
|
||||
if (link != null)
|
||||
desc = $"[link]({link.Uri}) " + desc;
|
||||
|
||||
var img = item.Links.FirstOrDefault(x => x.RelationshipType == "enclosure")?.Uri.AbsoluteUri
|
||||
?? Regex.Match(item.Description, @"src=""(?<src>.*?)""").Groups["src"].ToString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(img) && Uri.IsWellFormedUriString(img, UriKind.Absolute))
|
||||
embed.WithImageUrl(img);
|
||||
|
||||
embed.AddField(title, desc);
|
||||
|
||||
//send the created embed to all subscribed channels
|
||||
var sendTasks = kvp.Value
|
||||
.Where(x => x.GuildConfig != null)
|
||||
.Select(x => _client.GetGuild(x.GuildConfig.GuildId)
|
||||
?.GetTextChannel(x.ChannelId))
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.EmbedAsync(embed));
|
||||
|
||||
_lastPosts.AddOrUpdate(kvp.Key, item.Published.UtcDateTime, (k, old) => item.Published.UtcDateTime);
|
||||
|
||||
await Task.WhenAll(sendTasks).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
await Task.Delay(10000);
|
||||
}
|
||||
}
|
||||
|
||||
public List<FeedSub> GetFeeds(ulong guildId)
|
||||
{
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
return uow.GuildConfigs.For(guildId, set => set.Include(x => x.FeedSubs))
|
||||
.FeedSubs
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddFeed(ulong guildId, ulong channelId, string rssFeed)
|
||||
{
|
||||
rssFeed.ThrowIfNull(nameof(rssFeed));
|
||||
|
||||
var fs = new FeedSub()
|
||||
{
|
||||
ChannelId = channelId,
|
||||
Url = rssFeed.Trim().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
var gc = uow.GuildConfigs.For(guildId, set => set.Include(x => x.FeedSubs));
|
||||
|
||||
if (gc.FeedSubs.Contains(fs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (gc.FeedSubs.Count >= 10)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
gc.FeedSubs.Add(fs);
|
||||
|
||||
//adding all, in case bot wasn't on this guild when it started
|
||||
foreach (var f in gc.FeedSubs)
|
||||
{
|
||||
_subs.AddOrUpdate(f.Url, new HashSet<FeedSub>(), (k, old) =>
|
||||
{
|
||||
old.Add(f);
|
||||
return old;
|
||||
});
|
||||
}
|
||||
|
||||
uow.Complete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveFeed(ulong guildId, int index)
|
||||
{
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
var items = uow.GuildConfigs.For(guildId, set => set.Include(x => x.FeedSubs))
|
||||
.FeedSubs
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (items.Count <= index)
|
||||
return false;
|
||||
var toRemove = items[index];
|
||||
_subs.AddOrUpdate(toRemove.Url, new HashSet<FeedSub>(), (key, old) =>
|
||||
{
|
||||
old.Remove(toRemove);
|
||||
return old;
|
||||
});
|
||||
uow._context.Remove(toRemove);
|
||||
uow.Complete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
251
NadekoBot.Core/Modules/Searches/Services/SearchesService.cs
Normal file
251
NadekoBot.Core/Modules/Searches/Services/SearchesService.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using AngleSharp;
|
||||
using System.Threading;
|
||||
using NadekoBot.Modules.Searches.Exceptions;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services
|
||||
{
|
||||
public class SearchesService : INService, IUnloadableService
|
||||
{
|
||||
public HttpClient Http { get; }
|
||||
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IGoogleApiService _google;
|
||||
private readonly DbService _db;
|
||||
private readonly Logger _log;
|
||||
|
||||
public ConcurrentDictionary<ulong, bool> TranslatedChannels { get; } = new ConcurrentDictionary<ulong, bool>();
|
||||
public ConcurrentDictionary<UserChannelPair, string> UserLanguages { get; } = new ConcurrentDictionary<UserChannelPair, string>();
|
||||
|
||||
public readonly string PokemonAbilitiesFile = "data/pokemon/pokemon_abilities7.json";
|
||||
public readonly string PokemonListFile = "data/pokemon/pokemon_list7.json";
|
||||
public Dictionary<string, SearchPokemon> Pokemons { get; } = new Dictionary<string, SearchPokemon>();
|
||||
public Dictionary<string, SearchPokemonAbility> PokemonAbilities { get; } = new Dictionary<string, SearchPokemonAbility>();
|
||||
|
||||
public List<WoWJoke> WowJokes { get; } = new List<WoWJoke>();
|
||||
public List<MagicItem> MagicItems { get; } = new List<MagicItem>();
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, SearchImageCacher> _imageCacher = new ConcurrentDictionary<ulong, SearchImageCacher>();
|
||||
|
||||
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
||||
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
||||
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, HashSet<string>> _blacklistedTags = new ConcurrentDictionary<ulong, HashSet<string>>();
|
||||
|
||||
public SearchesService(DiscordSocketClient client, IGoogleApiService google,
|
||||
DbService db, NadekoBot bot)
|
||||
{
|
||||
Http = new HttpClient();
|
||||
Http.AddFakeHeaders();
|
||||
_client = client;
|
||||
_google = google;
|
||||
_db = db;
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
_blacklistedTags = new ConcurrentDictionary<ulong, HashSet<string>>(
|
||||
bot.AllGuildConfigs.ToDictionary(
|
||||
x => x.GuildId,
|
||||
x => new HashSet<string>(x.NsfwBlacklistedTags.Select(y => y.Tag))));
|
||||
|
||||
//translate commands
|
||||
_client.MessageReceived += (msg) =>
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var umsg = msg as SocketUserMessage;
|
||||
if (umsg == null)
|
||||
return;
|
||||
|
||||
if (!TranslatedChannels.TryGetValue(umsg.Channel.Id, out var autoDelete))
|
||||
return;
|
||||
|
||||
var key = new UserChannelPair()
|
||||
{
|
||||
UserId = umsg.Author.Id,
|
||||
ChannelId = umsg.Channel.Id,
|
||||
};
|
||||
|
||||
if (!UserLanguages.TryGetValue(key, out string langs))
|
||||
return;
|
||||
|
||||
var text = await Translate(langs, umsg.Resolve(TagHandling.Ignore))
|
||||
.ConfigureAwait(false);
|
||||
if (autoDelete)
|
||||
try { await umsg.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
await umsg.Channel.SendConfirmAsync($"{umsg.Author.Mention} `:` " + text.Replace("<@ ", "<@").Replace("<@! ", "<@!")).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
//pokemon commands
|
||||
if (File.Exists(PokemonListFile))
|
||||
{
|
||||
Pokemons = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(PokemonListFile));
|
||||
}
|
||||
else
|
||||
_log.Warn(PokemonListFile + " is missing. Pokemon abilities not loaded.");
|
||||
if (File.Exists(PokemonAbilitiesFile))
|
||||
PokemonAbilities = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(File.ReadAllText(PokemonAbilitiesFile));
|
||||
else
|
||||
_log.Warn(PokemonAbilitiesFile + " is missing. Pokemon abilities not loaded.");
|
||||
|
||||
//joke commands
|
||||
if (File.Exists("data/wowjokes.json"))
|
||||
{
|
||||
WowJokes = JsonConvert.DeserializeObject<List<WoWJoke>>(File.ReadAllText("data/wowjokes.json"));
|
||||
}
|
||||
else
|
||||
_log.Warn("data/wowjokes.json is missing. WOW Jokes are not loaded.");
|
||||
|
||||
if (File.Exists("data/magicitems.json"))
|
||||
{
|
||||
MagicItems = JsonConvert.DeserializeObject<List<MagicItem>>(File.ReadAllText("data/magicitems.json"));
|
||||
}
|
||||
else
|
||||
_log.Warn("data/magicitems.json is missing. Magic items are not loaded.");
|
||||
}
|
||||
|
||||
public async Task<string> Translate(string langs, string text = null)
|
||||
{
|
||||
var langarr = langs.ToLowerInvariant().Split('>');
|
||||
if (langarr.Length != 2)
|
||||
throw new ArgumentException();
|
||||
var from = langarr[0];
|
||||
var to = langarr[1];
|
||||
text = text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
throw new ArgumentException();
|
||||
return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions();
|
||||
}
|
||||
|
||||
public Task<ImageCacherObject> DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false)
|
||||
{
|
||||
if (guild.HasValue)
|
||||
{
|
||||
var blacklistedTags = GetBlacklistedTags(guild.Value);
|
||||
|
||||
if (blacklistedTags
|
||||
.Any(x => tag.ToLowerInvariant().Contains(x)))
|
||||
{
|
||||
throw new TagBlacklistedException();
|
||||
}
|
||||
|
||||
var cacher = _imageCacher.GetOrAdd(guild.Value, (key) => new SearchImageCacher());
|
||||
|
||||
return cacher.GetImage(tag, isExplicit, type, blacklistedTags);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher());
|
||||
|
||||
return cacher.GetImage(tag, isExplicit, type);
|
||||
}
|
||||
}
|
||||
|
||||
public HashSet<string> GetBlacklistedTags(ulong guildId)
|
||||
{
|
||||
if (_blacklistedTags.TryGetValue(guildId, out var tags))
|
||||
return tags;
|
||||
return new HashSet<string>();
|
||||
}
|
||||
|
||||
public bool ToggleBlacklistedTag(ulong guildId, string tag)
|
||||
{
|
||||
var tagObj = new NsfwBlacklitedTag
|
||||
{
|
||||
Tag = tag
|
||||
};
|
||||
|
||||
bool added;
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
var gc = uow.GuildConfigs.For(guildId, set => set.Include(y => y.NsfwBlacklistedTags));
|
||||
if (gc.NsfwBlacklistedTags.Add(tagObj))
|
||||
added = true;
|
||||
else
|
||||
{
|
||||
gc.NsfwBlacklistedTags.Remove(tagObj);
|
||||
added = false;
|
||||
}
|
||||
var newTags = new HashSet<string>(gc.NsfwBlacklistedTags.Select(x => x.Tag));
|
||||
_blacklistedTags.AddOrUpdate(guildId, newTags, delegate { return newTags; });
|
||||
|
||||
uow.Complete();
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
foreach (var c in _imageCacher)
|
||||
{
|
||||
c.Value?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetYomamaJoke()
|
||||
{
|
||||
var response = await Http.GetStringAsync("http://api.yomomma.info/").ConfigureAwait(false);
|
||||
return JObject.Parse(response)["joke"].ToString() + " 😆";
|
||||
}
|
||||
|
||||
public async Task<(string Text, string BaseUri)> GetRandomJoke()
|
||||
{
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var document = await BrowsingContext.New(config).OpenAsync("http://www.goodbadjokes.com/random");
|
||||
|
||||
var html = document.QuerySelector(".post > .joke-content");
|
||||
|
||||
var part1 = html.QuerySelector("dt").TextContent;
|
||||
var part2 = html.QuerySelector("dd").TextContent;
|
||||
|
||||
return (part1 + "\n\n" + part2, document.BaseUri);
|
||||
}
|
||||
|
||||
public async Task<string> GetChuckNorrisJoke()
|
||||
{
|
||||
var response = await Http.GetStringAsync("http://api.icndb.com/jokes/random/").ConfigureAwait(false);
|
||||
return JObject.Parse(response)["value"]["joke"].ToString() + " 😆";
|
||||
}
|
||||
|
||||
public Task Unload()
|
||||
{
|
||||
AutoBoobTimers.ForEach(x => x.Value.Change(Timeout.Infinite, Timeout.Infinite));
|
||||
AutoBoobTimers.Clear();
|
||||
AutoButtTimers.ForEach(x => x.Value.Change(Timeout.Infinite, Timeout.Infinite));
|
||||
AutoButtTimers.Clear();
|
||||
AutoHentaiTimers.ForEach(x => x.Value.Change(Timeout.Infinite, Timeout.Infinite));
|
||||
AutoHentaiTimers.Clear();
|
||||
|
||||
_imageCacher.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public struct UserChannelPair
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Common.Exceptions;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services
|
||||
{
|
||||
public class StreamNotificationService : INService
|
||||
{
|
||||
private readonly Timer _streamCheckTimer;
|
||||
private bool firstStreamNotifPass { get; set; } = true;
|
||||
private readonly ConcurrentDictionary<string, IStreamResponse> _cachedStatuses = new ConcurrentDictionary<string, IStreamResponse>();
|
||||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public StreamNotificationService(DbService db, DiscordSocketClient client, NadekoStrings strings)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
_strings = strings;
|
||||
_http = new HttpClient();
|
||||
_streamCheckTimer = new Timer(async (state) =>
|
||||
{
|
||||
var oldCachedStatuses = new ConcurrentDictionary<string, IStreamResponse>(_cachedStatuses);
|
||||
_cachedStatuses.Clear();
|
||||
IEnumerable<FollowedStream> streams;
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
streams = uow.GuildConfigs.GetAllFollowedStreams(client.Guilds.Select(x => (long)x.Id).ToList());
|
||||
}
|
||||
|
||||
await Task.WhenAll(streams.Select(async fs =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var newStatus = await GetStreamStatus(fs).ConfigureAwait(false);
|
||||
if (firstStreamNotifPass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IStreamResponse oldResponse;
|
||||
if (oldCachedStatuses.TryGetValue(newStatus.Url, out oldResponse) &&
|
||||
oldResponse.Live != newStatus.Live)
|
||||
{
|
||||
var server = _client.GetGuild(fs.GuildId);
|
||||
var channel = server?.GetTextChannel(fs.ChannelId);
|
||||
if (channel == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
await channel.EmbedAsync(GetEmbed(fs, newStatus, channel.Guild.Id)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}));
|
||||
|
||||
firstStreamNotifPass = false;
|
||||
}, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
public async Task<IStreamResponse> GetStreamStatus(FollowedStream stream, bool checkCache = true)
|
||||
{
|
||||
string response;
|
||||
IStreamResponse result;
|
||||
switch (stream.Type)
|
||||
{
|
||||
case FollowedStream.FollowedStreamType.Smashcast:
|
||||
var smashcastUrl = $"https://api.smashcast.tv/user/{stream.Username.ToLowerInvariant()}";
|
||||
if (checkCache && _cachedStatuses.TryGetValue(smashcastUrl, out result))
|
||||
return result;
|
||||
response = await _http.GetStringAsync(smashcastUrl).ConfigureAwait(false);
|
||||
|
||||
var scData = JsonConvert.DeserializeObject<SmashcastResponse>(response);
|
||||
if (!scData.Success)
|
||||
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
||||
scData.Url = smashcastUrl;
|
||||
_cachedStatuses.AddOrUpdate(smashcastUrl, scData, (key, old) => scData);
|
||||
return scData;
|
||||
case FollowedStream.FollowedStreamType.Twitch:
|
||||
var twitchUrl = $"https://api.twitch.tv/kraken/streams/{Uri.EscapeUriString(stream.Username.ToLowerInvariant())}?client_id=67w6z9i09xv2uoojdm9l0wsyph4hxo6";
|
||||
if (checkCache && _cachedStatuses.TryGetValue(twitchUrl, out result))
|
||||
return result;
|
||||
response = await _http.GetStringAsync(twitchUrl).ConfigureAwait(false);
|
||||
|
||||
var twData = JsonConvert.DeserializeObject<TwitchResponse>(response);
|
||||
if (twData.Error != null)
|
||||
{
|
||||
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
||||
}
|
||||
twData.Url = twitchUrl;
|
||||
_cachedStatuses.AddOrUpdate(twitchUrl, twData, (key, old) => twData);
|
||||
return twData;
|
||||
case FollowedStream.FollowedStreamType.Mixer:
|
||||
var beamUrl = $"https://mixer.com/api/v1/channels/{stream.Username.ToLowerInvariant()}";
|
||||
if (checkCache && _cachedStatuses.TryGetValue(beamUrl, out result))
|
||||
return result;
|
||||
response = await _http.GetStringAsync(beamUrl).ConfigureAwait(false);
|
||||
|
||||
|
||||
var bmData = JsonConvert.DeserializeObject<MixerResponse>(response);
|
||||
if (bmData.Error != null)
|
||||
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
||||
bmData.Url = beamUrl;
|
||||
_cachedStatuses.AddOrUpdate(beamUrl, bmData, (key, old) => bmData);
|
||||
return bmData;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public EmbedBuilder GetEmbed(FollowedStream fs, IStreamResponse status, ulong guildId)
|
||||
{
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle(fs.Username)
|
||||
.WithUrl(GetLink(fs))
|
||||
.WithDescription(GetLink(fs))
|
||||
.AddField(efb => efb.WithName(GetText(fs, "status"))
|
||||
.WithValue(status.Live ? "Online" : "Offline")
|
||||
.WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText(fs, "viewers"))
|
||||
.WithValue(status.Live ? status.Viewers.ToString() : "-")
|
||||
.WithIsInline(true))
|
||||
.WithColor(status.Live ? NadekoBot.OkColor : NadekoBot.ErrorColor);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status.Title))
|
||||
embed.WithAuthor(status.Title);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status.Game))
|
||||
embed.AddField(GetText(fs, "streaming"),
|
||||
status.Game,
|
||||
true);
|
||||
|
||||
embed.AddField(GetText(fs, "followers"),
|
||||
status.FollowerCount.ToString(),
|
||||
true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status.Icon))
|
||||
embed.WithThumbnailUrl(status.Icon);
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
public string GetText(FollowedStream fs, string key, params object[] replacements) =>
|
||||
_strings.GetText(key,
|
||||
fs.GuildId,
|
||||
"Searches".ToLowerInvariant(),
|
||||
replacements);
|
||||
|
||||
public string GetLink(FollowedStream fs)
|
||||
{
|
||||
if (fs.Type == FollowedStream.FollowedStreamType.Smashcast)
|
||||
return $"https://www.smashcast.tv/{fs.Username}/";
|
||||
if (fs.Type == FollowedStream.FollowedStreamType.Twitch)
|
||||
return $"https://www.twitch.tv/{fs.Username}/";
|
||||
if (fs.Type == FollowedStream.FollowedStreamType.Mixer)
|
||||
return $"https://www.mixer.com/{fs.Username}/";
|
||||
return "??";
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user