Files
nadekobot/src/Nadeko.Bot.Modules.Searches/_common/StreamNotifications/Providers/TwitchHelixProvider.cs
2023-07-16 16:37:58 +00:00

197 lines
6.4 KiB
C#

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