mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 18:28:27 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
12
src/NadekoBot/Modules/Searches/_common/AtlExtensions.cs
Normal file
12
src/NadekoBot/Modules/Searches/_common/AtlExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
#nullable disable
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public static class AtlExtensions
|
||||
{
|
||||
public static Task<AutoTranslateChannel> GetByChannelId(this IQueryable<AutoTranslateChannel> set, ulong channelId)
|
||||
=> set.Include(x => x.Users).FirstOrDefaultAsyncEF(x => x.ChannelId == channelId);
|
||||
}
|
21
src/NadekoBot/Modules/Searches/_common/BibleVerses.cs
Normal file
21
src/NadekoBot/Modules/Searches/_common/BibleVerses.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class BibleVerses
|
||||
{
|
||||
public string Error { get; set; }
|
||||
public BibleVerse[] Verses { get; set; }
|
||||
}
|
||||
|
||||
public class BibleVerse
|
||||
{
|
||||
[JsonPropertyName("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,7 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public enum ImgSearchEngine
|
||||
{
|
||||
Google,
|
||||
Searx,
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
using Cloneable;
|
||||
using NadekoBot.Common.Yml;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
[Cloneable]
|
||||
public partial class SearchesConfig : ICloneable<SearchesConfig>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 0;
|
||||
|
||||
[Comment("""
|
||||
Which engine should .search command
|
||||
'google_scrape' - default. Scrapes the webpage for results. May break. Requires no api keys.
|
||||
'google' - official google api. Requires googleApiKey and google.searchId set in creds.yml
|
||||
'searx' - requires at least one searx instance specified in the 'searxInstances' property below
|
||||
""")]
|
||||
public WebSearchEngine WebSearchEngine { get; set; } = WebSearchEngine.Google_Scrape;
|
||||
|
||||
[Comment("""
|
||||
Which engine should .image command use
|
||||
'google'- official google api. googleApiKey and google.imageSearchId set in creds.yml
|
||||
'searx' requires at least one searx instance specified in the 'searxInstances' property below
|
||||
""")]
|
||||
public ImgSearchEngine ImgSearchEngine { get; set; } = ImgSearchEngine.Google;
|
||||
|
||||
|
||||
[Comment("""
|
||||
Which search provider will be used for the `.youtube` command.
|
||||
|
||||
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console
|
||||
|
||||
- `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow.
|
||||
|
||||
- `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
|
||||
|
||||
- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
|
||||
""")]
|
||||
public YoutubeSearcher YtProvider { get; set; } = YoutubeSearcher.Ytdlp;
|
||||
|
||||
[Comment("""
|
||||
Set the searx instance urls in case you want to use 'searx' for either img or web search.
|
||||
Nadeko will use a random one for each request.
|
||||
Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`
|
||||
Instances specified must support 'format=json' query parameter.
|
||||
- In case you're running your own searx instance, set
|
||||
|
||||
search:
|
||||
formats:
|
||||
- json
|
||||
|
||||
in 'searxng/settings.yml' on your server
|
||||
|
||||
- If you're using a public instance, make sure that the instance you're using supports it (they usually don't)
|
||||
""")]
|
||||
public List<string> SearxInstances { get; set; } = new List<string>();
|
||||
|
||||
[Comment("""
|
||||
Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search
|
||||
Nadeko will use a random one for each request.
|
||||
These instances may be used for music queue functionality in the future.
|
||||
Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com
|
||||
|
||||
Instances specified must have api available.
|
||||
You check that by opening an api endpoint in your browser. For example: https://my-invidious-instance.mydomain.com/api/v1/trending
|
||||
""")]
|
||||
public List<string> InvidiousInstances { get; set; } = new List<string>();
|
||||
|
||||
[Comment("Maximum number of followed streams per server")]
|
||||
public FollowedStreamConfig FollowedStreams { get; set; } = new FollowedStreamConfig();
|
||||
}
|
||||
|
||||
public sealed class FollowedStreamConfig
|
||||
{
|
||||
[Comment("Maximum number of streams that each server can follow. -1 for infinite")]
|
||||
public int MaxCount { get; set; } = 10;
|
||||
}
|
||||
|
||||
public enum YoutubeSearcher
|
||||
{
|
||||
YtDataApiv3,
|
||||
Ytdl,
|
||||
Ytdlp,
|
||||
Invid,
|
||||
Invidious = 3
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
using NadekoBot.Common.Configs;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public class SearchesConfigService : ConfigServiceBase<SearchesConfig>
|
||||
{
|
||||
private static string FILE_PATH = "data/searches.yml";
|
||||
private static readonly TypedKey<SearchesConfig> _changeKey = new("config.searches.updated");
|
||||
|
||||
public override string Name
|
||||
=> "searches";
|
||||
|
||||
public SearchesConfigService(IConfigSeria serializer, IPubSub pubSub)
|
||||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
AddParsedProp("webEngine",
|
||||
sc => sc.WebSearchEngine,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("imgEngine",
|
||||
sc => sc.ImgSearchEngine,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("ytProvider",
|
||||
sc => sc.YtProvider,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("followedStreams.maxCount",
|
||||
sc => sc.FollowedStreams.MaxCount,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
if (data.Version < 1)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 1;
|
||||
c.WebSearchEngine = WebSearchEngine.Google_Scrape;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 2)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 2;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public enum WebSearchEngine
|
||||
{
|
||||
Google,
|
||||
Google_Scrape,
|
||||
Searx,
|
||||
}
|
66
src/NadekoBot/Modules/Searches/_common/CryptoData.cs
Normal file
66
src/NadekoBot/Modules/Searches/_common/CryptoData.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class CryptoResponse
|
||||
{
|
||||
public List<CmcResponseData> Data { get; set; }
|
||||
}
|
||||
|
||||
public class CmcQuote
|
||||
{
|
||||
[JsonPropertyName("price")]
|
||||
public double Price { get; set; }
|
||||
|
||||
[JsonPropertyName("volume_24h")]
|
||||
public double Volume24h { get; set; }
|
||||
|
||||
// [JsonPropertyName("volume_change_24h")]
|
||||
// public double VolumeChange24h { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("percent_change_1h")]
|
||||
// public double PercentChange1h { get; set; }
|
||||
|
||||
[JsonPropertyName("percent_change_24h")]
|
||||
public double PercentChange24h { get; set; }
|
||||
|
||||
[JsonPropertyName("percent_change_7d")]
|
||||
public double PercentChange7d { get; set; }
|
||||
|
||||
[JsonPropertyName("market_cap")]
|
||||
public double MarketCap { get; set; }
|
||||
|
||||
[JsonPropertyName("market_cap_dominance")]
|
||||
public double MarketCapDominance { get; set; }
|
||||
}
|
||||
|
||||
public class CmcResponseData
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("symbol")]
|
||||
public string Symbol { get; set; }
|
||||
|
||||
[JsonPropertyName("slug")]
|
||||
public string Slug { get; set; }
|
||||
|
||||
[JsonPropertyName("cmc_rank")]
|
||||
public int CmcRank { get; set; }
|
||||
|
||||
[JsonPropertyName("circulating_supply")]
|
||||
public double? CirculatingSupply { get; set; }
|
||||
|
||||
[JsonPropertyName("total_supply")]
|
||||
public double? TotalSupply { get; set; }
|
||||
|
||||
[JsonPropertyName("max_supply")]
|
||||
public double? MaxSupply { get; set; }
|
||||
|
||||
[JsonPropertyName("quote")]
|
||||
public Dictionary<string, CmcQuote> Quote { get; set; }
|
||||
}
|
43
src/NadekoBot/Modules/Searches/_common/DefineModel.cs
Normal file
43
src/NadekoBot/Modules/Searches/_common/DefineModel.cs
Normal file
@@ -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; }
|
||||
}
|
24
src/NadekoBot/Modules/Searches/_common/E621Object.cs
Normal file
24
src/NadekoBot/Modules/Searches/_common/E621Object.cs
Normal file
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
9
src/NadekoBot/Modules/Searches/_common/Extensions.cs
Normal file
9
src/NadekoBot/Modules/Searches/_common/Extensions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static StreamDataKey CreateKey(this FollowedStream fs)
|
||||
=> new(fs.Type, fs.Username.ToLower());
|
||||
}
|
44
src/NadekoBot/Modules/Searches/_common/Gallery.cs
Normal file
44
src/NadekoBot/Modules/Searches/_common/Gallery.cs
Normal file
@@ -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 uint 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(
|
||||
uint 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;
|
||||
}
|
||||
}
|
52
src/NadekoBot/Modules/Searches/_common/GatariUserResponse.cs
Normal file
52
src/NadekoBot/Modules/Searches/_common/GatariUserResponse.cs
Normal file
@@ -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; }
|
||||
}
|
16
src/NadekoBot/Modules/Searches/_common/GoogleSearchResult.cs
Normal file
16
src/NadekoBot/Modules/Searches/_common/GoogleSearchResult.cs
Normal file
@@ -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,12 @@
|
||||
#nullable disable
|
||||
using System.Text.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class LowerCaseNamingPolicy : JsonNamingPolicy
|
||||
{
|
||||
public static LowerCaseNamingPolicy Default = new();
|
||||
|
||||
public override string ConvertName(string name)
|
||||
=> name.ToLower();
|
||||
}
|
8
src/NadekoBot/Modules/Searches/_common/MagicItem.cs
Normal file
8
src/NadekoBot/Modules/Searches/_common/MagicItem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class MagicItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
26
src/NadekoBot/Modules/Searches/_common/MtgData.cs
Normal file
26
src/NadekoBot/Modules/Searches/_common/MtgData.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
14
src/NadekoBot/Modules/Searches/_common/NovelData.cs
Normal file
14
src/NadekoBot/Modules/Searches/_common/NovelData.cs
Normal file
@@ -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; }
|
||||
}
|
13
src/NadekoBot/Modules/Searches/_common/OmdbMovie.cs
Normal file
13
src/NadekoBot/Modules/Searches/_common/OmdbMovie.cs
Normal file
@@ -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; }
|
||||
}
|
9
src/NadekoBot/Modules/Searches/_common/OsuMapData.cs
Normal file
9
src/NadekoBot/Modules/Searches/_common/OsuMapData.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class OsuMapData
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Artist { get; set; }
|
||||
public string Version { get; set; }
|
||||
}
|
58
src/NadekoBot/Modules/Searches/_common/OsuUserBets.cs
Normal file
58
src/NadekoBot/Modules/Searches/_common/OsuUserBets.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class OsuUserBests
|
||||
{
|
||||
[JsonProperty("beatmap_id")]
|
||||
public string BeatmapId { get; set; }
|
||||
|
||||
[JsonProperty("score_id")]
|
||||
public string ScoreId { get; set; }
|
||||
|
||||
[JsonProperty("score")]
|
||||
public string Score { get; set; }
|
||||
|
||||
[JsonProperty("maxcombo")]
|
||||
public string Maxcombo { get; set; }
|
||||
|
||||
[JsonProperty("count50")]
|
||||
public double Count50 { get; set; }
|
||||
|
||||
[JsonProperty("count100")]
|
||||
public double Count100 { get; set; }
|
||||
|
||||
[JsonProperty("count300")]
|
||||
public double Count300 { get; set; }
|
||||
|
||||
[JsonProperty("countmiss")]
|
||||
public int Countmiss { get; set; }
|
||||
|
||||
[JsonProperty("countkatu")]
|
||||
public double Countkatu { get; set; }
|
||||
|
||||
[JsonProperty("countgeki")]
|
||||
public double Countgeki { get; set; }
|
||||
|
||||
[JsonProperty("perfect")]
|
||||
public string Perfect { get; set; }
|
||||
|
||||
[JsonProperty("enabled_mods")]
|
||||
public int EnabledMods { get; set; }
|
||||
|
||||
[JsonProperty("user_id")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonProperty("date")]
|
||||
public string Date { get; set; }
|
||||
|
||||
[JsonProperty("rank")]
|
||||
public string Rank { get; set; }
|
||||
|
||||
[JsonProperty("pp")]
|
||||
public double Pp { get; set; }
|
||||
|
||||
[JsonProperty("replay_available")]
|
||||
public string ReplayAvailable { get; set; }
|
||||
}
|
70
src/NadekoBot/Modules/Searches/_common/OsuUserData.cs
Normal file
70
src/NadekoBot/Modules/Searches/_common/OsuUserData.cs
Normal file
@@ -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; }
|
||||
}
|
40
src/NadekoBot/Modules/Searches/_common/PathOfExileModels.cs
Normal file
40
src/NadekoBot/Modules/Searches/_common/PathOfExileModels.cs
Normal file
@@ -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; }
|
||||
}
|
35
src/NadekoBot/Modules/Searches/_common/SteamGameId.cs
Normal file
35
src/NadekoBot/Modules/Searches/_common/SteamGameId.cs
Normal file
@@ -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,64 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class HelixStreamsResponse
|
||||
{
|
||||
public class PaginationData
|
||||
{
|
||||
[JsonPropertyName("cursor")]
|
||||
public string Cursor { get; set; }
|
||||
}
|
||||
|
||||
public class StreamData
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("user_login")]
|
||||
public string UserLogin { get; set; }
|
||||
|
||||
[JsonPropertyName("user_name")]
|
||||
public string UserName { get; set; }
|
||||
|
||||
[JsonPropertyName("game_id")]
|
||||
public string GameId { get; set; }
|
||||
|
||||
[JsonPropertyName("game_name")]
|
||||
public string GameName { 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("started_at")]
|
||||
public DateTime StartedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
[JsonPropertyName("thumbnail_url")]
|
||||
public string ThumbnailUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("tag_ids")]
|
||||
public List<string> TagIds { get; set; }
|
||||
|
||||
[JsonPropertyName("is_mature")]
|
||||
public bool IsMature { get; set; }
|
||||
}
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public List<StreamData> Data { get; set; }
|
||||
|
||||
[JsonPropertyName("pagination")]
|
||||
public PaginationData Pagination { get; set; }
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class HelixUsersResponse
|
||||
{
|
||||
public class User
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("login")]
|
||||
public string Login { get; set; }
|
||||
|
||||
[JsonPropertyName("display_name")]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("broadcaster_type")]
|
||||
public string BroadcasterType { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("profile_image_url")]
|
||||
public string ProfileImageUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("offline_image_url")]
|
||||
public string OfflineImageUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("view_count")]
|
||||
public int ViewCount { get; set; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public List<User> Data { get; set; }
|
||||
}
|
@@ -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 record 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,16 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public readonly struct StreamDataKey
|
||||
{
|
||||
public FollowedStream.FType Type { get; init; }
|
||||
public string Name { get; init; }
|
||||
|
||||
public StreamDataKey(FollowedStream.FType type, string name)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoGetUsersResponse
|
||||
{
|
||||
[JsonPropertyName("is_live")]
|
||||
public bool IsLive { get; set; }
|
||||
|
||||
[JsonPropertyName("category_id")]
|
||||
public string CategoryId { get; set; }
|
||||
|
||||
[JsonPropertyName("category_name")]
|
||||
public string CategoryName { get; set; }
|
||||
|
||||
[JsonPropertyName("live_title")]
|
||||
public string LiveTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("audi_type")]
|
||||
public string AudiType { get; set; }
|
||||
|
||||
[JsonPropertyName("language_code")]
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
[JsonPropertyName("thumbnail")]
|
||||
public string Thumbnail { get; set; }
|
||||
|
||||
[JsonPropertyName("current_viewers")]
|
||||
public int CurrentViewers { get; set; }
|
||||
|
||||
[JsonPropertyName("followers")]
|
||||
public int Followers { get; set; }
|
||||
|
||||
[JsonPropertyName("streamer_info")]
|
||||
public string StreamerInfo { get; set; }
|
||||
|
||||
[JsonPropertyName("profile_pic")]
|
||||
public string ProfilePic { get; set; }
|
||||
|
||||
[JsonPropertyName("channel_url")]
|
||||
public string ChannelUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("subscriber_num")]
|
||||
public int SubscriberNum { get; set; }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonPropertyName("social_links")]
|
||||
public List<TrovoSocialLink> SocialLinks { get; set; }
|
||||
|
||||
[JsonPropertyName("started_at")]
|
||||
public string StartedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("ended_at")]
|
||||
public string EndedAt { get; set; }
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoRequestData
|
||||
{
|
||||
[JsonPropertyName("username")]
|
||||
public string Username { get; set; }
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoSocialLink
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { 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,215 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
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 IReadOnlyDictionary<FollowedStream.FType, Provider> _streamProviders;
|
||||
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
|
||||
private readonly ConcurrentDictionary<StreamDataKey, StreamData?> _cache = new();
|
||||
|
||||
public NotifChecker(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IBotCredsProvider credsProvider)
|
||||
{
|
||||
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
|
||||
{
|
||||
{ FollowedStream.FType.Twitch, new TwitchHelixProvider(httpClientFactory, credsProvider) },
|
||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
||||
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
|
||||
};
|
||||
_offlineBuffer = new();
|
||||
}
|
||||
|
||||
// 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.Value > duration)
|
||||
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key)))
|
||||
.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 = GetAllData();
|
||||
|
||||
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<IReadOnlyCollection<StreamData>>(
|
||||
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();
|
||||
|
||||
// compare old data with new data
|
||||
if (!oldStreamDataDict.TryGetValue(key.Type, out var typeDict)
|
||||
|| !typeDict.TryGetValue(key.Name, out var oldData)
|
||||
|| oldData is null)
|
||||
{
|
||||
AddLastData(key, newData, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// fill with last known game in case it's empty
|
||||
if (string.IsNullOrWhiteSpace(newData.Game))
|
||||
newData.Game = oldData.Game;
|
||||
|
||||
AddLastData(key, newData, true);
|
||||
|
||||
// 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 AddLastData(StreamDataKey key, StreamData? data, bool replace)
|
||||
{
|
||||
if (replace)
|
||||
{
|
||||
_cache[key] = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return _cache.TryAdd(key, data);
|
||||
}
|
||||
|
||||
public void DeleteLastData(StreamDataKey key)
|
||||
=> _cache.TryRemove(key, out _);
|
||||
|
||||
public Dictionary<StreamDataKey, StreamData?> GetAllData()
|
||||
=> _cache.ToDictionary(x => x.Key, x => x.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 AddLastData(data.CreateKey(), data, false);
|
||||
}
|
||||
|
||||
public void UntrackStreamByKey(in StreamDataKey key)
|
||||
=> DeleteLastData(key);
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
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 login)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string>
|
||||
{
|
||||
login
|
||||
});
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
return new List<StreamData>();
|
||||
|
||||
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
|
||||
using 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,63 @@
|
||||
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.
|
||||
/// Override to provide a custom implementation
|
||||
/// </summary>
|
||||
public virtual IReadOnlyDictionary<string, DateTime> FailingStreams
|
||||
=> _failingStreams;
|
||||
|
||||
/// <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="login">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 login);
|
||||
|
||||
/// <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<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames);
|
||||
|
||||
/// <summary>
|
||||
/// Unmark the stream as errored. You should override this method
|
||||
/// if you've overridden the <see cref="FailingStreams"/> property.
|
||||
/// </summary>
|
||||
/// <param name="login"></param>
|
||||
public virtual void ClearErrorsFor(string login)
|
||||
=> _failingStreams.Clear();
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
|
||||
public class TrovoProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Trovo;
|
||||
|
||||
private readonly Regex _urlRegex = new(@"trovo.live\/(?<channel>[\w\d\-_]+)/?", RegexOptions.Compiled);
|
||||
|
||||
private readonly IBotCredsProvider _creds;
|
||||
|
||||
|
||||
public TrovoProvider(IHttpClientFactory httpClientFactory, IBotCredsProvider creds)
|
||||
{
|
||||
(_httpClientFactory, _creds) = (httpClientFactory, creds);
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(creds.GetCreds().TrovoClientId))
|
||||
{
|
||||
Log.Warning("""
|
||||
Trovo streams are using a default clientId.
|
||||
If you are experiencing ratelimits, you should create your own application at: https://developer.trovo.live/
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
=> Task.FromResult(_urlRegex.IsMatch(url));
|
||||
|
||||
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
{
|
||||
var match = _urlRegex.Match(url);
|
||||
if (match.Length == 0)
|
||||
return Task.FromResult(default(StreamData?));
|
||||
|
||||
return GetStreamDataAsync(match.Groups["channel"].Value);
|
||||
}
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string login)
|
||||
{
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
|
||||
var trovoClientId = _creds.GetCreds().TrovoClientId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trovoClientId))
|
||||
{
|
||||
trovoClientId = "8b3cc4719b7051803099661a3265e50b";
|
||||
}
|
||||
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
http.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
http.DefaultRequestHeaders.Add("Client-ID", trovoClientId);
|
||||
|
||||
// trovo ratelimit is very generous (1200 per minute)
|
||||
// so there is no need for ratelimit checks atm
|
||||
try
|
||||
{
|
||||
using var res = await http.PostAsJsonAsync(
|
||||
$"https://open-api.trovo.live/openplatform/channels/id",
|
||||
new TrovoRequestData()
|
||||
{
|
||||
Username = login
|
||||
});
|
||||
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
var data = await res.Content.ReadFromJsonAsync<TrovoGetUsersResponse>();
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
Log.Warning("An empty response received while retrieving stream data for trovo.live/{TrovoId}", login);
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
return null;
|
||||
}
|
||||
|
||||
_failingStreams.TryRemove(data.Username, out _);
|
||||
return new()
|
||||
{
|
||||
IsLive = data.IsLive,
|
||||
Game = data.CategoryName,
|
||||
Name = data.Username,
|
||||
Title = data.LiveTitle,
|
||||
Viewers = data.CurrentViewers,
|
||||
AvatarUrl = data.ProfilePic,
|
||||
StreamType = Platform,
|
||||
StreamUrl = data.ChannelUrl,
|
||||
UniqueName = data.Username,
|
||||
Preview = data.Thumbnail
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error retrieving stream data for trovo.live/{TrovoId}", login);
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames)
|
||||
{
|
||||
var trovoClientId = _creds.GetCreds().TrovoClientId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trovoClientId))
|
||||
{
|
||||
Log.Warning("Trovo streams will be ignored until TrovoClientId is added to creds.yml");
|
||||
return Array.Empty<StreamData>();
|
||||
}
|
||||
|
||||
var results = new List<StreamData>(usernames.Count);
|
||||
foreach (var chunk in usernames.Chunk(10)
|
||||
.Select(x => x.Select(GetStreamDataAsync)))
|
||||
{
|
||||
var chunkResults = await Task.WhenAll(chunk);
|
||||
results.AddRange(chunkResults.Where(x => x is not null)!);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
@@ -0,0 +1,197 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Text.RegularExpressions;
|
||||
using TwitchLib.Api;
|
||||
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(@"twitch.tv/(?<name>[\w\d\-_]+)/?",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override FollowedStream.FType Platform
|
||||
=> FollowedStream.FType.Twitch;
|
||||
|
||||
private readonly Lazy<TwitchAPI> _api;
|
||||
private readonly string _clientId;
|
||||
|
||||
public TwitchHelixProvider(IHttpClientFactory httpClientFactory, IBotCredsProvider credsProvider)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
|
||||
var creds = credsProvider.GetCreds();
|
||||
_clientId = creds.TwitchClientId;
|
||||
var clientSecret = creds.TwitchClientSecret;
|
||||
_api = new(() => new()
|
||||
{
|
||||
Helix =
|
||||
{
|
||||
Settings =
|
||||
{
|
||||
ClientId = _clientId,
|
||||
Secret = clientSecret
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string?> EnsureTokenValidAsync()
|
||||
=> await _api.Value.Auth.GetAccessTokenAsync();
|
||||
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
{
|
||||
var match = Regex.Match(url);
|
||||
if (!match.Success)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
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 login)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string>
|
||||
{
|
||||
login
|
||||
});
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
{
|
||||
return Array.Empty<StreamData>();
|
||||
}
|
||||
|
||||
var token = await EnsureTokenValidAsync();
|
||||
|
||||
if (token is null)
|
||||
{
|
||||
Log.Warning("Twitch client id and client secret key are not added to creds.yml or incorrect");
|
||||
return Array.Empty<StreamData>();
|
||||
}
|
||||
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
http.DefaultRequestHeaders.Add("Client-Id", _clientId);
|
||||
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
|
||||
|
||||
var loginsSet = logins.Select(x => x.ToLowerInvariant())
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
|
||||
var dataDict = new Dictionary<string, StreamData>();
|
||||
|
||||
foreach (var chunk in logins.Chunk(100))
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = await http.GetStringAsync($"https://api.twitch.tv/helix/users"
|
||||
+ $"?{chunk.Select(x => $"login={x}").Join('&')}"
|
||||
+ $"&first=100");
|
||||
|
||||
var resObj = JsonSerializer.Deserialize<HelixUsersResponse>(str);
|
||||
|
||||
if (resObj?.Data is null || resObj.Data.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var user in resObj.Data)
|
||||
{
|
||||
var lowerLogin = user.Login.ToLowerInvariant();
|
||||
if (loginsSet.Remove(lowerLogin))
|
||||
{
|
||||
dataDict[lowerLogin] = UserToStreamData(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Something went wrong retreiving {StreamPlatform} streams", Platform);
|
||||
return new List<StreamData>();
|
||||
}
|
||||
}
|
||||
|
||||
// any item left over loginsSet is an invalid username
|
||||
foreach (var login in loginsSet)
|
||||
{
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
// only get streams for users which exist
|
||||
foreach (var chunk in dataDict.Keys.Chunk(100))
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = await http.GetStringAsync($"https://api.twitch.tv/helix/streams"
|
||||
+ $"?{chunk.Select(x => $"user_login={x}").Join('&')}"
|
||||
+ "&first=100");
|
||||
|
||||
var res = JsonSerializer.Deserialize<HelixStreamsResponse>(str);
|
||||
|
||||
if (res?.Data is null || res.Data.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var helixStreamData in res.Data)
|
||||
{
|
||||
var login = helixStreamData.UserLogin.ToLowerInvariant();
|
||||
if (dataDict.TryGetValue(login, out var old))
|
||||
{
|
||||
dataDict[login] = FillStreamData(old, helixStreamData);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Something went wrong retreiving {StreamPlatform} streams", Platform);
|
||||
return new List<StreamData>();
|
||||
}
|
||||
}
|
||||
|
||||
return dataDict.Values;
|
||||
}
|
||||
|
||||
private StreamData UserToStreamData(HelixUsersResponse.User user)
|
||||
=> new()
|
||||
{
|
||||
UniqueName = user.Login,
|
||||
Name = user.DisplayName,
|
||||
AvatarUrl = user.ProfileImageUrl,
|
||||
IsLive = false,
|
||||
StreamUrl = $"https://twitch.tv/{user.Login}",
|
||||
StreamType = FollowedStream.FType.Twitch,
|
||||
Preview = user.OfflineImageUrl
|
||||
};
|
||||
|
||||
private StreamData FillStreamData(StreamData partial, HelixStreamsResponse.StreamData apiData)
|
||||
=> partial with
|
||||
{
|
||||
StreamType = FollowedStream.FType.Twitch,
|
||||
Viewers = apiData.ViewerCount,
|
||||
Title = apiData.Title,
|
||||
IsLive = apiData.Type == "live",
|
||||
Preview = apiData.ThumbnailUrl
|
||||
.Replace("{width}", "640")
|
||||
.Replace("{height}", "480"),
|
||||
Game = apiData.GameName,
|
||||
};
|
||||
}
|
9
src/NadekoBot/Modules/Searches/_common/TimeData.cs
Normal file
9
src/NadekoBot/Modules/Searches/_common/TimeData.cs
Normal file
@@ -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; }
|
||||
}
|
22
src/NadekoBot/Modules/Searches/_common/TimeModels.cs
Normal file
22
src/NadekoBot/Modules/Searches/_common/TimeModels.cs
Normal file
@@ -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; }
|
||||
}
|
14
src/NadekoBot/Modules/Searches/_common/UrbanDef.cs
Normal file
14
src/NadekoBot/Modules/Searches/_common/UrbanDef.cs
Normal file
@@ -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; }
|
||||
}
|
67
src/NadekoBot/Modules/Searches/_common/WeatherModels.cs
Normal file
67
src/NadekoBot/Modules/Searches/_common/WeatherModels.cs
Normal file
@@ -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; }
|
||||
}
|
18
src/NadekoBot/Modules/Searches/_common/WikipediaApiModel.cs
Normal file
18
src/NadekoBot/Modules/Searches/_common/WikipediaApiModel.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
11
src/NadekoBot/Modules/Searches/_common/WoWJoke.cs
Normal file
11
src/NadekoBot/Modules/Searches/_common/WoWJoke.cs
Normal file
@@ -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}**";
|
||||
}
|
Reference in New Issue
Block a user