Initial implementation of trovo.live notifications support for .stadd

This commit is contained in:
Kwoth
2022-01-31 00:47:37 +01:00
parent cd812304f7
commit 0f36242597
19 changed files with 318 additions and 84 deletions

View File

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

View File

@@ -0,0 +1,10 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class TrovoRequestData
{
[JsonPropertyName("channel_id")]
public string ChannelId { get; set; }
}

View File

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

View File

@@ -17,6 +17,7 @@ public class NotifChecker
public NotifChecker(
IHttpClientFactory httpClientFactory,
IBotCredsProvider credsProvider,
ConnectionMultiplexer multi,
string uniqueCacheKey,
bool isMaster)
@@ -26,7 +27,8 @@ public class NotifChecker
_streamProviders = new()
{
{ FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) },
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) }
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
};
_offlineBuffer = new();
if (isMaster) CacheClearAllData();
@@ -35,17 +37,20 @@ public class NotifChecker
// 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();
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;
}
@@ -54,6 +59,7 @@ public class NotifChecker
=> Task.Run(async () =>
{
while (true)
{
try
{
var allStreamData = CacheGetAllData();
@@ -65,19 +71,21 @@ public class NotifChecker
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());
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();
// 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>();
@@ -124,7 +132,10 @@ public class NotifChecker
}
}
var tasks = new List<Task> { Task.Delay(30_000) };
var tasks = new List<Task>
{
Task.Delay(30_000)
};
if (newlyOnline.Count > 0) tasks.Add(OnStreamsOnline(newlyOnline));
@@ -136,6 +147,7 @@ public class NotifChecker
{
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
}
}
});
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)

View File

@@ -46,10 +46,10 @@ public class PicartoProvider : Provider
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new();
return new List<StreamData>();
using var http = _httpClientFactory.CreateClient();
var toReturn = new List<StreamData>();

View File

@@ -15,9 +15,10 @@ public abstract class Provider
/// <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 IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams
=> _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
public virtual IReadOnlyDictionary<string, DateTime> FailingStreams
=> _failingStreams;
/// <summary>
/// When was the first time the stream continually had errors while being retrieved
@@ -50,8 +51,13 @@ public abstract class Provider
/// </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 abstract Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames);
public void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
/// <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();
}

View File

@@ -0,0 +1,106 @@
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 Regex(@"trovo.live\/(?<channel>[\w\d\-_]+)/?", RegexOptions.Compiled);
private readonly IBotCredsProvider _creds;
public TrovoProvider(IHttpClientFactory httpClientFactory, IBotCredsProvider creds)
=> (_httpClientFactory, _creds) = (httpClientFactory, creds);
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 id)
{
using var http = _httpClientFactory.CreateClient();
var trovoClientId = _creds.GetCreds().TrovoClientId;
if (string.IsNullOrWhiteSpace(trovoClientId))
trovoClientId = "waiting for key";
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
{
var res = await http.PostAsJsonAsync(
$"https://open-api.trovo.live/openplatform/channels/id",
new TrovoRequestData()
{
ChannelId = id
});
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}", id);
_failingStreams.TryAdd(id, DateTime.UtcNow);
return null;
}
return new()
{
IsLive = data.IsLive,
Game = data.CategoryName,
Name = data.Username,
Title = data.LiveTitle,
Viewers = data.CurrentViewers,
AvatarUrl = data.ProfilePic,
StreamType = FollowedStream.FType.Picarto,
StreamUrl = data.ChannelUrl,
UniqueName = data.Username,
Preview = data.Thumbnail,
};
}
catch (Exception ex)
{
Log.Warning(ex, "Error retrieving stream data for trovo.live/{TrovoId}", id);
_failingStreams.TryAdd(id, DateTime.UtcNow);
return null;
}
}
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames)
{
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;
}
}

View File

@@ -12,11 +12,17 @@ public class TwitchProvider : Provider
public override FollowedStream.FType Platform
=> FollowedStream.FType.Twitch;
public override IReadOnlyDictionary<string, DateTime> FailingStreams
=> _failingStreams;
private readonly IHttpClientFactory _httpClientFactory;
public TwitchProvider(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
public override void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
public override Task<bool> IsValidUrl(string url)
{
var match = Regex.Match(url);
@@ -41,15 +47,18 @@ public class TwitchProvider : Provider
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
var data = await GetStreamDataAsync(new List<string> { id });
var data = await GetStreamDataAsync(new List<string>
{
id
});
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new();
return new List<StreamData>();
using var http = _httpClientFactory.CreateClient();
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
@@ -70,7 +79,11 @@ public class TwitchProvider : Provider
// 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() });
var resObj = JsonConvert.DeserializeAnonymousType(str,
new
{
Stream = new TwitchResponseV5.Stream()
});
// if stream is null, user is not streaming
if (resObj?.Stream is null)