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/(?[\w\d\-_]+)/?", RegexOptions.Compiled | RegexOptions.IgnoreCase); public override FollowedStream.FType Platform => FollowedStream.FType.Twitch; private readonly Lazy _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 EnsureTokenValidAsync() => await _api.Value.Auth.GetAccessTokenAsync(); public override Task IsValidUrl(string url) { var match = Regex.Match(url); if (!match.Success) { return Task.FromResult(false); } return Task.FromResult(true); } public override Task GetStreamDataByUrlAsync(string url) { var match = Regex.Match(url); if (match.Success) { var name = match.Groups["name"].Value; return GetStreamDataAsync(name); } return Task.FromResult(null); } public override async Task GetStreamDataAsync(string login) { var data = await GetStreamDataAsync(new List { login }); return data.FirstOrDefault(); } public override async Task> GetStreamDataAsync(List logins) { if (logins.Count == 0) { return Array.Empty(); } 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(); } 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(); 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(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(); } } // 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(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(); } } 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, }; }