Fixed some crashes in response strings source generator, reorganized more submodules into their folders

This commit is contained in:
Kwoth
2022-01-02 03:49:54 +01:00
parent 9c590668df
commit 4b6af0e4ef
191 changed files with 120 additions and 80 deletions
@@ -0,0 +1,20 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class BibleVerses
{
public string Error { get; set; }
public BibleVerse[] Verses { get; set; }
}
public class BibleVerse
{
[JsonProperty("book_name")]
public string BookName { get; set; }
public int Chapter { get; set; }
public int Verse { get; set; }
public string Text { get; set; }
}
@@ -0,0 +1,37 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class CryptoResponse
{
public List<CryptoResponseData> Data { get; set; }
}
public class CryptoResponseData
{
public string Id { get; set; }
public string Name { get; set; }
public string Symbol { get; set; }
public string Slug { get; set; }
[JsonProperty("cmc_rank")]
public int Rank { get; set; }
public CurrencyQuotes Quote { get; set; }
}
public class CurrencyQuotes
{
public Quote Usd { get; set; }
}
public class Quote
{
public double Price { get; set; }
public double Market_Cap { get; set; }
public string Percent_Change_1h { get; set; }
public string Percent_Change_24h { get; set; }
public string Percent_Change_7d { get; set; }
public double? Volume_24h { get; set; }
}
@@ -0,0 +1,43 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class Audio
{
public string Url { get; set; }
}
public class Example
{
public List<Audio> Audio { get; set; }
public string Text { get; set; }
}
public class GramaticalInfo
{
public string Type { get; set; }
}
public class Sens
{
public object Definition { get; set; }
public List<Example> Examples { get; set; }
[JsonProperty("gramatical_info")]
public GramaticalInfo GramaticalInfo { get; set; }
}
public class Result
{
[JsonProperty("part_of_speech")]
public string PartOfSpeech { get; set; }
public List<Sens> Senses { get; set; }
public string Url { get; set; }
}
public class DefineModel
{
public List<Result> Results { get; set; }
}
@@ -0,0 +1,24 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class E621Object
{
public FileData File { get; set; }
public TagData Tags { get; set; }
public ScoreData Score { get; set; }
public class FileData
{
public string Url { get; set; }
}
public class TagData
{
public string[] General { get; set; }
}
public class ScoreData
{
public string Total { get; set; }
}
}
@@ -0,0 +1,19 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common.Exceptions;
public class StreamNotFoundException : Exception
{
public StreamNotFoundException()
{
}
public StreamNotFoundException(string message)
: base(message)
{
}
public StreamNotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}
}
@@ -0,0 +1,44 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public sealed class Tag
{
public string Name { get; set; }
public string Url { get; set; }
}
public sealed class Gallery
{
public string Id { get; }
public string Url { get; }
public string FullTitle { get; }
public string Title { get; }
public string Thumbnail { get; }
public int PageCount { get; }
public int Likes { get; }
public DateTime UploadedAt { get; }
public Tag[] Tags { get; }
public Gallery(
string id,
string url,
string fullTitle,
string title,
string thumbnail,
int pageCount,
int likes,
DateTime uploadedAt,
Tag[] tags)
{
Id = id;
Url = url;
FullTitle = fullTitle;
Title = title;
Thumbnail = thumbnail;
PageCount = pageCount;
Likes = likes;
UploadedAt = uploadedAt;
Tags = tags;
}
}
@@ -0,0 +1,52 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class UserData
{
[JsonProperty("abbr")]
public object Abbr { get; set; }
[JsonProperty("clanid")]
public object Clanid { get; set; }
[JsonProperty("country")]
public string Country { get; set; }
[JsonProperty("favourite_mode")]
public int FavouriteMode { get; set; }
[JsonProperty("followers_count")]
public int FollowersCount { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("latest_activity")]
public int LatestActivity { get; set; }
[JsonProperty("play_style")]
public int PlayStyle { get; set; }
[JsonProperty("privileges")]
public int Privileges { get; set; }
[JsonProperty("registered_on")]
public int RegisteredOn { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("username_aka")]
public string UsernameAka { get; set; }
}
public class GatariUserResponse
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("users")]
public List<UserData> Users { get; set; }
}
@@ -0,0 +1,76 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class UserStats
{
[JsonProperty("a_count")]
public int ACount { get; set; }
[JsonProperty("avg_accuracy")]
public double AvgAccuracy { get; set; }
[JsonProperty("avg_hits_play")]
public double AvgHitsPlay { get; set; }
[JsonProperty("country_rank")]
public int CountryRank { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("level")]
public int Level { get; set; }
[JsonProperty("level_progress")]
public int LevelProgress { get; set; }
[JsonProperty("max_combo")]
public int MaxCombo { get; set; }
[JsonProperty("playcount")]
public int Playcount { get; set; }
[JsonProperty("playtime")]
public int Playtime { get; set; }
[JsonProperty("pp")]
public int Pp { get; set; }
[JsonProperty("rank")]
public int Rank { get; set; }
[JsonProperty("ranked_score")]
public int RankedScore { get; set; }
[JsonProperty("replays_watched")]
public int ReplaysWatched { get; set; }
[JsonProperty("s_count")]
public int SCount { get; set; }
[JsonProperty("sh_count")]
public int ShCount { get; set; }
[JsonProperty("total_hits")]
public int TotalHits { get; set; }
[JsonProperty("total_score")]
public long TotalScore { get; set; }
[JsonProperty("x_count")]
public int XCount { get; set; }
[JsonProperty("xh_count")]
public int XhCount { get; set; }
}
public class GatariUserStatsResponse
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("stats")]
public UserStats Stats { get; set; }
}
@@ -0,0 +1,16 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public sealed class GoogleSearchResult
{
public string Title { get; }
public string Link { get; }
public string Text { get; }
public GoogleSearchResult(string title, string link, string text)
{
Title = title;
Link = link;
Text = text;
}
}
@@ -0,0 +1,13 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class HearthstoneCardData
{
public string Text { get; set; }
public string Flavor { get; set; }
public bool Collectible { get; set; }
public string Img { get; set; }
public string ImgGold { get; set; }
public string PlayerClass { get; set; }
}
@@ -0,0 +1,43 @@
#nullable disable
using NadekoBot.Modules.Nsfw.Common;
namespace NadekoBot.Modules.Searches.Common;
public class ImageCacherObject : IComparable<ImageCacherObject>
{
public Booru SearchType { get; }
public string FileUrl { get; }
public HashSet<string> Tags { get; }
public string Rating { get; }
public ImageCacherObject(DapiImageObject obj, Booru type)
{
if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
else
FileUrl = obj.FileUrl.StartsWith("http", StringComparison.InvariantCulture)
? obj.FileUrl
: "https:" + obj.FileUrl;
SearchType = type;
Rating = obj.Rating;
Tags = new((obj.Tags ?? obj.TagString).Split(' '));
}
public ImageCacherObject(
string url,
Booru type,
string tags,
string rating)
{
SearchType = type;
FileUrl = url;
Tags = new(tags.Split(' '));
Rating = rating;
}
public override string ToString()
=> FileUrl;
public int CompareTo(ImageCacherObject other)
=> string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
}
@@ -0,0 +1,12 @@
#nullable disable
using System.Text.Json;
namespace SystemTextJsonSamples;
public class LowerCaseNamingPolicy : JsonNamingPolicy
{
public static LowerCaseNamingPolicy Default = new();
public override string ConvertName(string name)
=> name.ToLower();
}
@@ -0,0 +1,8 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class MagicItem
{
public string Name { get; set; }
public string Description { get; set; }
}
@@ -0,0 +1,26 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class MtgData
{
public string Name { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; }
public string StoreUrl { get; set; }
public string Types { get; set; }
public string ManaCost { get; set; }
}
public class MtgResponse
{
public List<Data> Cards { get; set; }
public class Data
{
public string Name { get; set; }
public string ManaCost { get; set; }
public string Text { get; set; }
public List<string> Types { get; set; }
public string ImageUrl { get; set; }
}
}
@@ -0,0 +1,121 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public static class NhentaiApiModel
{
public class Title
{
[JsonProperty("english")]
public string English { get; set; }
[JsonProperty("japanese")]
public string Japanese { get; set; }
[JsonProperty("pretty")]
public string Pretty { get; set; }
}
public class Page
{
[JsonProperty("t")]
public string T { get; set; }
[JsonProperty("w")]
public int W { get; set; }
[JsonProperty("h")]
public int H { get; set; }
}
public class Cover
{
[JsonProperty("t")]
public string T { get; set; }
[JsonProperty("w")]
public int W { get; set; }
[JsonProperty("h")]
public int H { get; set; }
}
public class Thumbnail
{
[JsonProperty("t")]
public string T { get; set; }
[JsonProperty("w")]
public int W { get; set; }
[JsonProperty("h")]
public int H { get; set; }
}
public class Images
{
[JsonProperty("pages")]
public List<Page> Pages { get; set; }
[JsonProperty("cover")]
public Cover Cover { get; set; }
[JsonProperty("thumbnail")]
public Thumbnail Thumbnail { get; set; }
}
public class Tag
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("count")]
public int Count { get; set; }
}
public class Gallery
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("media_id")]
public string MediaId { get; set; }
[JsonProperty("title")]
public Title Title { get; set; }
[JsonProperty("images")]
public Images Images { get; set; }
[JsonProperty("scanlator")]
public string Scanlator { get; set; }
[JsonProperty("upload_date")]
public double UploadDate { get; set; }
[JsonProperty("tags")]
public Tag[] Tags { get; set; }
[JsonProperty("num_pages")]
public int NumPages { get; set; }
[JsonProperty("num_favorites")]
public int NumFavorites { get; set; }
}
public class SearchResult
{
[JsonProperty("result")]
public Gallery[] Result { get; set; }
}
}
@@ -0,0 +1,14 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class NovelResult
{
public string Description { get; set; }
public string Title { get; set; }
public string Link { get; set; }
public string ImageUrl { get; set; }
public string[] Authors { get; set; }
public string Status { get; set; }
public string[] Genres { get; set; }
public string Score { get; set; }
}
@@ -0,0 +1,13 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class OmdbMovie
{
public string Title { get; set; }
public string Year { get; set; }
public string ImdbRating { get; set; }
public string ImdbId { get; set; }
public string Genre { get; set; }
public string Plot { get; set; }
public string Poster { get; set; }
}
@@ -0,0 +1,70 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class OsuUserData
{
[JsonProperty("user_id")]
public string UserId { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("join_date")]
public string JoinDate { get; set; }
[JsonProperty("count300")]
public string Count300 { get; set; }
[JsonProperty("count100")]
public string Count100 { get; set; }
[JsonProperty("count50")]
public string Count50 { get; set; }
[JsonProperty("playcount")]
public string Playcount { get; set; }
[JsonProperty("ranked_score")]
public string RankedScore { get; set; }
[JsonProperty("total_score")]
public string TotalScore { get; set; }
[JsonProperty("pp_rank")]
public string PpRank { get; set; }
[JsonProperty("level")]
public double Level { get; set; }
[JsonProperty("pp_raw")]
public double PpRaw { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("count_rank_ss")]
public string CountRankSs { get; set; }
[JsonProperty("count_rank_ssh")]
public string CountRankSsh { get; set; }
[JsonProperty("count_rank_s")]
public string CountRankS { get; set; }
[JsonProperty("count_rank_sh")]
public string CountRankSh { get; set; }
[JsonProperty("count_rank_a")]
public string CountRankA { get; set; }
[JsonProperty("country")]
public string Country { get; set; }
[JsonProperty("total_seconds_played")]
public string TotalSecondsPlayed { get; set; }
[JsonProperty("pp_country_rank")]
public string PpCountryRank { get; set; }
}
@@ -0,0 +1,40 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class Account
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("league")]
public string League { get; set; }
[JsonProperty("classId")]
public int ClassId { get; set; }
[JsonProperty("ascendancyClass")]
public int AscendancyClass { get; set; }
[JsonProperty("class")]
public string Class { get; set; }
[JsonProperty("level")]
public int Level { get; set; }
}
public class Leagues
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("startAt")]
public DateTime StartAt { get; set; }
[JsonProperty("endAt")]
public object EndAt { get; set; }
}
@@ -0,0 +1,35 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches;
public class SteamGameId
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("appid")]
public int AppId { get; set; }
}
public class SteamGameData
{
public string ShortDescription { get; set; }
public class Container
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("data")]
public SteamGameData Data { get; set; }
}
}
public enum TimeErrors
{
InvalidInput,
ApiKeyMissing,
NotFound,
Unknown
}
@@ -0,0 +1,157 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class PicartoChannelResponse
{
[JsonProperty("user_id")]
public int UserId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("avatar")]
public string Avatar { get; set; }
[JsonProperty("online")]
public bool Online { get; set; }
[JsonProperty("viewers")]
public int Viewers { get; set; }
[JsonProperty("viewers_total")]
public int ViewersTotal { get; set; }
[JsonProperty("thumbnails")]
public Thumbnails Thumbnails { get; set; }
[JsonProperty("followers")]
public int Followers { get; set; }
[JsonProperty("subscribers")]
public int Subscribers { get; set; }
[JsonProperty("adult")]
public bool Adult { get; set; }
[JsonProperty("category")]
public string Category { get; set; }
[JsonProperty("account_type")]
public string AccountType { get; set; }
[JsonProperty("commissions")]
public bool Commissions { get; set; }
[JsonProperty("recordings")]
public bool Recordings { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("description_panels")]
public List<DescriptionPanel> DescriptionPanels { get; set; }
[JsonProperty("private")]
public bool Private { get; set; }
[JsonProperty("private_message")]
public string PrivateMessage { get; set; }
[JsonProperty("gaming")]
public bool Gaming { get; set; }
[JsonProperty("chat_settings")]
public ChatSettings ChatSettings { get; set; }
[JsonProperty("last_live")]
public DateTime LastLive { get; set; }
[JsonProperty("tags")]
public List<string> Tags { get; set; }
[JsonProperty("multistream")]
public List<Multistream> Multistream { get; set; }
[JsonProperty("languages")]
public List<Language> Languages { get; set; }
[JsonProperty("following")]
public bool Following { get; set; }
}
public class Thumbnails
{
[JsonProperty("web")]
public string Web { get; set; }
[JsonProperty("web_large")]
public string WebLarge { get; set; }
[JsonProperty("mobile")]
public string Mobile { get; set; }
[JsonProperty("tablet")]
public string Tablet { get; set; }
}
public class DescriptionPanel
{
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("body")]
public string Body { get; set; }
[JsonProperty("image")]
public string Image { get; set; }
[JsonProperty("image_link")]
public string ImageLink { get; set; }
[JsonProperty("button_text")]
public string ButtonText { get; set; }
[JsonProperty("button_link")]
public string ButtonLink { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
}
public class ChatSettings
{
[JsonProperty("guest_chat")]
public bool GuestChat { get; set; }
[JsonProperty("links")]
public bool Links { get; set; }
[JsonProperty("level")]
public int Level { get; set; }
}
public class Multistream
{
[JsonProperty("user_id")]
public int UserId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("online")]
public bool Online { get; set; }
[JsonProperty("adult")]
public bool Adult { get; set; }
}
public class Language
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
@@ -0,0 +1,21 @@
#nullable disable
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Searches.Common;
public class StreamData
{
public FollowedStream.FType StreamType { get; set; }
public string Name { get; set; }
public string UniqueName { get; set; }
public int Viewers { get; set; }
public string Title { get; set; }
public string Game { get; set; }
public string Preview { get; set; }
public bool IsLive { get; set; }
public string StreamUrl { get; set; }
public string AvatarUrl { get; set; }
public StreamDataKey CreateKey()
=> new(StreamType, UniqueName.ToLower());
}
@@ -0,0 +1,18 @@
#nullable disable
using NadekoBot.Db.Models;
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches.Common;
public readonly struct StreamDataKey
{
public FollowedStream.FType Type { get; }
public string Name { get; }
[JsonConstructor]
public StreamDataKey(FollowedStream.FType type, string name)
{
Type = type;
Name = name;
}
}
@@ -0,0 +1,22 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
//
// public class TwitchResponse
// {
// public List<StreamApiData> Data { get; set; }
//
// public class StreamApiData
// {
// public string Id { get; set; }
// public string UserId { get; set; }
// public string UserName { get; set; }
// public string GameId { get; set; }
// public string Type { get; set; }
// public string Title { get; set; }
// public int ViewerCount { get; set; }
// public string Language { get; set; }
// public string ThumbnailUrl { get; set; }
// public DateTime StartedAt { get; set; }
// }
// }
@@ -0,0 +1,114 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class TwitchResponseV5
{
public List<Stream> Streams { get; set; }
public class Channel
{
[JsonProperty("_id")]
public int Id { get; set; }
[JsonProperty("broadcaster_language")]
public string BroadcasterLanguage { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
[JsonProperty("followers")]
public int Followers { get; set; }
[JsonProperty("game")]
public string Game { get; set; }
[JsonProperty("language")]
public string Language { get; set; }
[JsonProperty("logo")]
public string Logo { get; set; }
[JsonProperty("mature")]
public bool Mature { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("partner")]
public bool Partner { get; set; }
[JsonProperty("profile_banner")]
public string ProfileBanner { get; set; }
[JsonProperty("profile_banner_background_color")]
public object ProfileBannerBackgroundColor { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("updated_at")]
public DateTime UpdatedAt { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("video_banner")]
public string VideoBanner { get; set; }
[JsonProperty("views")]
public int Views { get; set; }
}
public class Preview
{
[JsonProperty("large")]
public string Large { get; set; }
[JsonProperty("medium")]
public string Medium { get; set; }
[JsonProperty("small")]
public string Small { get; set; }
[JsonProperty("template")]
public string Template { get; set; }
}
public class Stream
{
[JsonProperty("_id")]
public long Id { get; set; }
[JsonProperty("average_fps")]
public double AverageFps { get; set; }
[JsonProperty("channel")]
public Channel Channel { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("delay")]
public double Delay { get; set; }
[JsonProperty("game")]
public string Game { get; set; }
[JsonProperty("is_playlist")]
public bool IsPlaylist { get; set; }
[JsonProperty("preview")]
public Preview Preview { get; set; }
[JsonProperty("video_height")]
public int VideoHeight { get; set; }
[JsonProperty("viewers")]
public int Viewers { get; set; }
}
}
@@ -0,0 +1,37 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class TwitchUsersResponseV5
{
[JsonProperty("users")]
public List<User> Users { get; set; }
public class User
{
[JsonProperty("_id")]
public string Id { get; set; }
// [JsonProperty("bio")]
// public string Bio { get; set; }
//
// [JsonProperty("created_at")]
// public DateTime CreatedAt { get; set; }
//
// [JsonProperty("display_name")]
// public string DisplayName { get; set; }
//
// [JsonProperty("logo")]
// public string Logo { get; set; }
//
// [JsonProperty("name")]
// public string Name { get; set; }
//
// [JsonProperty("type")]
// public string Type { get; set; }
//
// [JsonProperty("updated_at")]
// public DateTime UpdatedAt { get; set; }
}
}
@@ -0,0 +1,221 @@
using NadekoBot.Db.Models;
using NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications;
public class NotifChecker
{
public event Func<List<StreamData>, Task> OnStreamsOffline = _ => Task.CompletedTask;
public event Func<List<StreamData>, Task> OnStreamsOnline = _ => Task.CompletedTask;
private readonly ConnectionMultiplexer _multi;
private readonly string _key;
private readonly Dictionary<FollowedStream.FType, Provider> _streamProviders;
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
public NotifChecker(
IHttpClientFactory httpClientFactory,
ConnectionMultiplexer multi,
string uniqueCacheKey,
bool isMaster)
{
_multi = multi;
_key = $"{uniqueCacheKey}_followed_streams_data";
_streamProviders = new()
{
{ FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) },
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) }
};
_offlineBuffer = new();
if (isMaster) CacheClearAllData();
}
// gets all streams which have been failing for more than the provided timespan
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
{
var toReturn = _streamProviders.SelectMany(prov => prov.Value
.FailingStreams
.Where(fs => DateTime.UtcNow - fs.ErroringSince
> duration)
.Select(fs => new StreamDataKey(prov.Value.Platform,
fs.Item1)))
.ToList();
if (remove)
foreach (var toBeRemoved in toReturn)
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
return toReturn;
}
public Task RunAsync()
=> Task.Run(async () =>
{
while (true)
try
{
var allStreamData = CacheGetAllData();
var oldStreamDataDict = allStreamData
// group by type
.GroupBy(entry => entry.Key.Type)
.ToDictionary(entry => entry.Key,
entry => entry.AsEnumerable()
.ToDictionary(x => x.Key.Name, x => x.Value));
var newStreamData = await oldStreamDataDict.Select(x =>
{
// get all stream data for the streams of this type
if (_streamProviders.TryGetValue(x.Key,
out var provider))
return provider.GetStreamDataAsync(x.Value
.Select(entry => entry.Key)
.ToList());
// this means there's no provider for this stream data, (and there was before?)
return Task.FromResult(new List<StreamData>());
})
.WhenAll();
var newlyOnline = new List<StreamData>();
var newlyOffline = new List<StreamData>();
// go through all new stream data, compare them with the old ones
foreach (var newData in newStreamData.SelectMany(x => x))
{
// update cached data
var key = newData.CreateKey();
CacheAddData(key, newData, true);
// compare old data with new data
var oldData = oldStreamDataDict[key.Type][key.Name];
// this is the first pass
if (oldData is null)
continue;
// if the stream is offline, we need to check if it was
// marked as offline once previously
// if it was, that means this is second time we're getting offline
// status for that stream -> notify subscribers
// Note: This is done because twitch api will sometimes return an offline status
// shortly after the stream is already online, which causes duplicate notifications.
// (stream is online -> stream is offline -> stream is online again (and stays online))
// This offlineBuffer will make it so that the stream has to be marked as offline TWICE
// before it sends an offline notification to the subscribers.
var streamId = (key.Type, key.Name);
if (!newData.IsLive && _offlineBuffer.Remove(streamId))
{
newlyOffline.Add(newData);
}
else if (newData.IsLive != oldData.IsLive)
{
if (newData.IsLive)
{
_offlineBuffer.Remove(streamId);
newlyOnline.Add(newData);
}
else
{
_offlineBuffer.Add(streamId);
// newlyOffline.Add(newData);
}
}
}
var tasks = new List<Task> { Task.Delay(30_000) };
if (newlyOnline.Count > 0) tasks.Add(OnStreamsOnline(newlyOnline));
if (newlyOffline.Count > 0) tasks.Add(OnStreamsOffline(newlyOffline));
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
}
});
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)
{
var db = _multi.GetDatabase();
return db.HashSet(_key,
JsonConvert.SerializeObject(key),
JsonConvert.SerializeObject(data),
replace ? When.Always : When.NotExists);
}
public void CacheDeleteData(StreamDataKey key)
{
var db = _multi.GetDatabase();
db.HashDelete(_key, JsonConvert.SerializeObject(key));
}
public void CacheClearAllData()
{
var db = _multi.GetDatabase();
db.KeyDelete(_key);
}
public Dictionary<StreamDataKey, StreamData?> CacheGetAllData()
{
var db = _multi.GetDatabase();
if (!db.KeyExists(_key)) return new();
return db.HashGetAll(_key)
.ToDictionary(entry => JsonConvert.DeserializeObject<StreamDataKey>(entry.Name),
entry => entry.Value.IsNullOrEmpty
? default
: JsonConvert.DeserializeObject<StreamData>(entry.Value));
}
public async Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
// loop through all providers and see which regex matches
foreach (var (_, provider) in _streamProviders)
{
var isValid = await provider.IsValidUrl(url);
if (!isValid)
continue;
// if it's not a valid url, try another provider
var data = await provider.GetStreamDataByUrlAsync(url);
return data;
}
// if no provider found, return null
return null;
}
/// <summary>
/// Return currently available stream data, get new one if none available, and start tracking the stream.
/// </summary>
/// <param name="url">Url of the stream</param>
/// <returns>Stream data, if any</returns>
public async Task<StreamData?> TrackStreamByUrlAsync(string url)
{
var data = await GetStreamDataByUrlAsync(url);
EnsureTracked(data);
return data;
}
/// <summary>
/// Make sure a stream is tracked using its stream data.
/// </summary>
/// <param name="data">Data to try to track if not already tracked</param>
/// <returns>Whether it's newly added</returns>
private bool EnsureTracked(StreamData? data)
{
// something failed, don't add anything to cache
if (data is null)
return false;
// if stream is found, add it to the cache for tracking only if it doesn't already exist
// because stream will be checked and events will fire in a loop. We don't want to override old state
return CacheAddData(data.CreateKey(), data, false);
}
public void UntrackStreamByKey(in StreamDataKey key)
=> CacheDeleteData(key);
}
@@ -0,0 +1,98 @@
using NadekoBot.Db.Models;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class PicartoProvider : Provider
{
private static Regex Regex { get; } = new(@"picarto.tv/(?<name>.+[^/])/?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override FollowedStream.FType Platform
=> FollowedStream.FType.Picarto;
private readonly IHttpClientFactory _httpClientFactory;
public PicartoProvider(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
public override Task<bool> IsValidUrl(string url)
{
var match = Regex.Match(url);
if (!match.Success)
return Task.FromResult(false);
// var username = match.Groups["name"].Value;
return Task.FromResult(true);
}
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
var match = Regex.Match(url);
if (match.Success)
{
var name = match.Groups["name"].Value;
return GetStreamDataAsync(name);
}
return Task.FromResult<StreamData?>(null);
}
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
var data = await GetStreamDataAsync(new List<string> { id });
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new();
using var http = _httpClientFactory.CreateClient();
var toReturn = new List<StreamData>();
foreach (var login in logins)
try
{
http.DefaultRequestHeaders.Accept.Add(new("application/json"));
// get id based on the username
var res = await http.GetAsync($"https://api.picarto.tv/v1/channel/name/{login}");
if (!res.IsSuccessStatusCode)
continue;
var userData =
JsonConvert.DeserializeObject<PicartoChannelResponse>(await res.Content.ReadAsStringAsync())!;
toReturn.Add(ToStreamData(userData));
_failingStreams.TryRemove(login, out _);
}
catch (Exception ex)
{
Log.Warning("Something went wrong retreiving {StreamPlatform} stream data for {Login}: {ErrorMessage}",
Platform,
login,
ex.Message);
_failingStreams.TryAdd(login, DateTime.UtcNow);
}
return toReturn;
}
private StreamData ToStreamData(PicartoChannelResponse stream)
=> new()
{
StreamType = FollowedStream.FType.Picarto,
Name = stream.Name,
UniqueName = stream.Name,
Viewers = stream.Viewers,
Title = stream.Title,
IsLive = stream.Online,
Preview = stream.Thumbnails.Web,
Game = stream.Category,
StreamUrl = $"https://picarto.tv/{stream.Name}",
AvatarUrl = stream.Avatar
};
}
@@ -0,0 +1,57 @@
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
/// <summary>
/// Abstract class implemented by providers of all supported platforms
/// </summary>
public abstract class Provider
{
/// <summary>
/// Type of the platform.
/// </summary>
public abstract FollowedStream.FType Platform { get; }
/// <summary>
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors.
/// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
/// </summary>
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams
=> _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
/// <summary>
/// When was the first time the stream continually had errors while being retrieved
/// </summary>
protected readonly ConcurrentDictionary<string, DateTime> _failingStreams = new();
/// <summary>
/// Checks whether the specified url is a valid stream url for this platform.
/// </summary>
/// <param name="url">Url to check</param>
/// <returns>True if valid, otherwise false</returns>
public abstract Task<bool> IsValidUrl(string url);
/// <summary>
/// Gets stream data of the stream on the specified url on this <see cref="Platform" />
/// </summary>
/// <param name="url">Url of the stream</param>
/// <returns><see cref="StreamData" /> of the specified stream. Null if none found</returns>
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
/// <summary>
/// Gets stream data of the specified id/username on this <see cref="Platform" />
/// </summary>
/// <param name="id">Name (or id where applicable) of the user on the platform</param>
/// <returns><see cref="StreamData" /> of the user. Null if none found</returns>
public abstract Task<StreamData?> GetStreamDataAsync(string id);
/// <summary>
/// Gets stream data of all specified ids/usernames on this <see cref="Platform" />
/// </summary>
/// <param name="usernames">List of ids/usernames</param>
/// <returns><see cref="StreamData" /> of all users, in the same order. Null for every id/user not found.</returns>
public abstract Task<List<StreamData>> GetStreamDataAsync(List<string> usernames);
public void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
}
@@ -0,0 +1,181 @@
#nullable disable
// using System;
// using System.Collections.Generic;
// using System.Linq;
// using System.Net.Http;
// using System.Text.Json;
// using System.Text.Json.Serialization;
// using System.Text.RegularExpressions;
// using System.Threading.Tasks;
// using NadekoBot.Services.Database.Models;
// using NadekoBot.Extensions;
// using Serilog;
// using JsonSerializer = System.Text.Json.JsonSerializer;
//
// namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers
// {
// public sealed class TwitchHelixProvider : Provider
// {
// private readonly IHttpClientFactory _httpClientFactory;
//
// //
// private static Regex Regex { get; } = new Regex(@"twitch.tv/(?<name>.+[^/])/?",
// RegexOptions.Compiled | RegexOptions.IgnoreCase);
//
// public override FollowedStream.FType Platform => FollowedStream.FType.Twitch;
//
// private (string Token, DateTime Expiry) _token = default;
//
// public TwitchHelixProvider(IHttpClientFactory httpClientFactory)
// {
// _httpClientFactory = httpClientFactory;
// }
//
// private async Task EnsureTokenValidAsync()
// {
// if (_token != default && (DateTime.UtcNow - _token.Expiry) > TimeSpan.FromHours(1))
// return;
//
// const string clientId = string.Empty;
// const string clientSecret = string.Empty;
//
// var client = _httpClientFactory.CreateClient();
// var res = await client.PostAsync("https://id.twitch.tv/oauth2/token" +
// $"?client_id={clientId}" +
// $"&client_secret={clientSecret}" +
// "&grant_type=client_credentials", new StringContent(""));
//
// var data = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
//
// _token = (data.GetProperty("access_token").GetString(),
// DateTime.UtcNow + TimeSpan.FromSeconds(data.GetProperty("expires_in").GetInt32()));
//
// }
//
// public override Task<bool> IsValidUrl(string url)
// {
// var match = Regex.Match(url);
// if (!match.Success)
// return Task.FromResult(false);
//
// var username = match.Groups["name"].Value;
// return Task.FromResult(true);
// }
//
// public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
// {
// var match = Regex.Match(url);
// if (match.Success)
// {
// var name = match.Groups["name"].Value;
// return GetStreamDataAsync(name);
// }
//
// return Task.FromResult<StreamData?>(null);
// }
//
// public override async Task<StreamData?> GetStreamDataAsync(string id)
// {
// var data = await GetStreamDataAsync(new List<string> {id});
//
// return data.FirstOrDefault();
// }
//
// public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
// {
// if (logins.Count == 0)
// return new List<StreamData>();
//
// await EnsureTokenValidAsync();
//
// using var http = _httpClientFactory.CreateClient();
// http.DefaultRequestHeaders.Clear();
// http.DefaultRequestHeaders.Add("client-id","67w6z9i09xv2uoojdm9l0wsyph4hxo6");
// http.DefaultRequestHeaders.Add("Authorization",$"Bearer {_token.Token}");
//
// var res = new TwitchResponse()
// {
// Data = new List<TwitchResponse.StreamApiData>()
// };
// foreach (var chunk in logins.Chunk(500))
// {
// try
// {
// var str = await http.GetStringAsync($"https://api.twitch.tv/helix/streams" +
// $"?user_login={chunk.JoinWith(',')}" +
// $"&first=100");
//
// res = JsonSerializer.Deserialize<TwitchResponse>(str);
// }
// catch (Exception ex)
// {
// Log.Warning(ex, "Something went wrong retreiving {StreamPlatform} streams", Platform);
// return new List<StreamData>();
// }
//
// if (res.Data.Count == 0)
// {
// return new List<StreamData>();
// }
// }
//
// return res.Data.Select(ToStreamData).ToList();
// }
//
// private StreamData ToStreamData(TwitchResponse.StreamApiData apiData)
// {
// return new StreamData()
// {
// StreamType = FollowedStream.FType.Twitch,
// Name = apiData.UserName,
// UniqueName = apiData.UserId,
// Viewers = apiData.ViewerCount,
// Title = apiData.Title,
// IsLive = apiData.Type == "live",
// Preview = apiData.ThumbnailUrl
// ?.Replace("{width}", "640")
// ?.Replace("{height}", "480"),
// Game = apiData.GameId,
// };
// }
// }
//
// public class TwitchResponse
// {
// [JsonPropertyName("data")]
// public List<StreamApiData> Data { get; set; }
//
// public class StreamApiData
// {
// [JsonPropertyName("id")]
// public string Id { get; set; }
//
// [JsonPropertyName("user_id")]
// public string UserId { get; set; }
//
// [JsonPropertyName("user_name")]
// public string UserName { get; set; }
//
// [JsonPropertyName("game_id")]
// public string GameId { get; set; }
//
// [JsonPropertyName("type")]
// public string Type { get; set; }
//
// [JsonPropertyName("title")]
// public string Title { get; set; }
//
// [JsonPropertyName("viewer_count")]
// public int ViewerCount { get; set; }
//
// [JsonPropertyName("language")]
// public string Language { get; set; }
//
// [JsonPropertyName("thumbnail_url")]
// public string ThumbnailUrl { get; set; }
//
// [JsonPropertyName("started_at")]
// public DateTime StartedAt { get; set; }
// }
// }
// }
@@ -0,0 +1,125 @@
using NadekoBot.Db.Models;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class TwitchProvider : Provider
{
private static Regex Regex { get; } = new(@"twitch.tv/(?<name>.+[^/])/?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override FollowedStream.FType Platform
=> FollowedStream.FType.Twitch;
private readonly IHttpClientFactory _httpClientFactory;
public TwitchProvider(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
public override Task<bool> IsValidUrl(string url)
{
var match = Regex.Match(url);
if (!match.Success)
return Task.FromResult(false);
// var username = match.Groups["name"].Value;
return Task.FromResult(true);
}
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
var match = Regex.Match(url);
if (match.Success)
{
var name = match.Groups["name"].Value;
return GetStreamDataAsync(name);
}
return Task.FromResult<StreamData?>(null);
}
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
var data = await GetStreamDataAsync(new List<string> { id });
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new();
using var http = _httpClientFactory.CreateClient();
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
http.DefaultRequestHeaders.Add("Accept", "application/vnd.twitchtv.v5+json");
var toReturn = new List<StreamData>();
foreach (var login in logins)
try
{
// get id based on the username
var idsStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/users?login={login}");
var userData = JsonConvert.DeserializeObject<TwitchUsersResponseV5>(idsStr);
var user = userData?.Users.FirstOrDefault();
// if user can't be found, skip, it means there is no such user
if (user is null)
continue;
// get stream data
var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}");
var resObj = JsonConvert.DeserializeAnonymousType(str, new { Stream = new TwitchResponseV5.Stream() });
// if stream is null, user is not streaming
if (resObj?.Stream is null)
{
// if user is not streaming, get his offline banner
var chStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/channels/{user.Id}");
var ch = JsonConvert.DeserializeObject<TwitchResponseV5.Channel>(chStr)!;
toReturn.Add(new()
{
StreamType = FollowedStream.FType.Twitch,
Name = ch.DisplayName,
UniqueName = ch.Name,
Title = ch.Status,
IsLive = false,
AvatarUrl = ch.Logo,
StreamUrl = $"https://twitch.tv/{ch.Name}",
Preview = ch.VideoBanner // set video banner as the preview,
});
continue; // move on
}
toReturn.Add(ToStreamData(resObj.Stream));
_failingStreams.TryRemove(login, out _);
}
catch (Exception ex)
{
Log.Warning("Something went wrong retreiving {StreamPlatform} stream data for {Login}: {ErrorMessage}",
Platform,
login,
ex.Message);
_failingStreams.TryAdd(login, DateTime.UtcNow);
}
return toReturn;
}
private StreamData ToStreamData(TwitchResponseV5.Stream stream)
=> new()
{
StreamType = FollowedStream.FType.Twitch,
Name = stream.Channel.DisplayName,
UniqueName = stream.Channel.Name,
Viewers = stream.Viewers,
Title = stream.Channel.Status,
IsLive = true,
Preview = stream.Preview.Large,
Game = stream.Channel.Game,
StreamUrl = $"https://twitch.tv/{stream.Channel.Name}",
AvatarUrl = stream.Channel.Logo
};
}
@@ -0,0 +1,9 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class TimeData
{
public string Address { get; set; }
public DateTime Time { get; set; }
public string TimeZoneName { get; set; }
}
@@ -0,0 +1,22 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class TimeZoneResult
{
[JsonProperty("abbreviation")]
public string TimezoneName { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
}
public class LocationIqResponse
{
public float Lat { get; set; }
public float Lon { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
}
@@ -0,0 +1,14 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class UrbanResponse
{
public UrbanDef[] List { get; set; }
}
public class UrbanDef
{
public string Word { get; set; }
public string Definition { get; set; }
public string Permalink { get; set; }
}
@@ -0,0 +1,67 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common;
public class Coord
{
public double Lon { get; set; }
public double Lat { get; set; }
}
public class Weather
{
public int Id { get; set; }
public string Main { get; set; }
public string Description { get; set; }
public string Icon { get; set; }
}
public class Main
{
public double Temp { get; set; }
public float Pressure { get; set; }
public float Humidity { get; set; }
[JsonProperty("temp_min")]
public double TempMin { get; set; }
[JsonProperty("temp_max")]
public double TempMax { get; set; }
}
public class Wind
{
public double Speed { get; set; }
public double Deg { get; set; }
}
public class Clouds
{
public int All { get; set; }
}
public class Sys
{
public int Type { get; set; }
public int Id { get; set; }
public double Message { get; set; }
public string Country { get; set; }
public double Sunrise { get; set; }
public double Sunset { get; set; }
}
public class WeatherData
{
public Coord Coord { get; set; }
public List<Weather> Weather { get; set; }
public Main Main { get; set; }
public int Visibility { get; set; }
public Wind Wind { get; set; }
public Clouds Clouds { get; set; }
public int Dt { get; set; }
public Sys Sys { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public int Cod { get; set; }
}
@@ -0,0 +1,18 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class WikipediaApiModel
{
public WikipediaQuery Query { get; set; }
public class WikipediaQuery
{
public WikipediaPage[] Pages { get; set; }
public class WikipediaPage
{
public bool Missing { get; set; } = false;
public string FullUrl { get; set; }
}
}
}
@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Modules.Searches.Common;
public class WoWJoke
{
public string Question { get; set; }
public string Answer { get; set; }
public override string ToString()
=> $"`{Question}`\n\n**{Answer}**";
}