public async Task <GameInfo> RefreshCategory(string broadcasterId, CancellationToken cancellationToken = default) { _logger.LogInformation("Starting category refresh for channelId {broadcasterId}", broadcasterId); try { var results = await _twitchAPIClient.SearchChannelsAsync(broadcasterId, cancellationToken); var newResults = results.First(c => c.BroadcasterLogin == broadcasterId); if (newResults.GameId != _lastResult?.GameId) { _lastInfo = await _twitchAPIClient.GetChannelInfoAsync(newResults.Id, cancellationToken); _gameInfo = await _gameLocalization.ResolveLocalizedGameInfoAsync(newResults.BroadcasterLanguage, newResults.GameId, cancellationToken); if (OnUpdate != null) { OnUpdate(this, _gameInfo); } } _lastResult = newResults; _broacasterLogin = broadcasterId; _lastCheck = DateTime.UtcNow; } catch (Exception ex) { _logger.LogError(ex, "Error refreshing category for channelId {broadcasterId}", broadcasterId); } return(_gameInfo); }
public async Task OnChannelUpdate(HelixChannelInfo info) { _channelState.State.LastCategoryId = info.GameId; _channelState.State.LastCategoryName = info.GameName; _channelState.State.LastLanguage = info.BroadcasterLanguage; _channelState.State.LastTitle = info.Title; await _channelState.WriteStateAsync(); if (_chatBot == null) { return; } var botContext = new ProcessorContext { ChannelId = info.BroadcasterId, ChannelName = info.BroadcasterName, Language = info.BroadcasterLanguage, CategoryId = info.GameId, }; var key = new CategoryKey { TwitchCategoryId = info.GameId, Locale = info.BroadcasterLanguage }; if (_categoriesState.State.Descriptions.TryGetValue(key, out var customDescription)) { botContext.CustomCategoryDescription = customDescription.Description; } else { botContext.CustomCategoryDescription = null; } await _chatBot.UpdateContext(botContext); }
async Task IChannelGrain.Activate(string userToken) { var userAuthenticated = Twitch.Authenticate() .FromOAuthToken(userToken) .Build(); var userClient = TwitchAPIClient.CreateFromBase(_appClient, userAuthenticated); var validated = await userClient.ValidateToken(); if (validated == null || validated.UserId != _channelId || validated.ExpiresIn == 0) { throw new ArgumentException("Could not validate token"); } _userClient = userClient; var channelInfoTask = _userClient.GetChannelInfoAsync(_channelId); List <HelixChannelModerator> moderators = new List <HelixChannelModerator>(); var editorsTask = _userClient.GetHelixChannelEditorsAsync(_channelId); await foreach (var moderator in _userClient.EnumerateChannelModeratorsAsync(_channelId)) { moderators.Add(moderator); } _channelState.State.BroadcasterToken = userToken; _channelState.State.Editors = (await editorsTask).ToList(); _channelState.State.Moderators = moderators.ToList(); await _channelState.WriteStateAsync(); var editorsTasks = _channelState.State.Editors.Select(editor => GrainFactory.GetGrain <IUserGrain>(editor.UserId).SetRole(new UserRole { Role = ChannelRole.Editor, ChannelId = _channelId, ChannelName = _channelInfo.BroadcasterName, }) ); var modsTasks = _channelState.State.Moderators.Select(moderator => GrainFactory.GetGrain <IUserGrain>(moderator.UserId).SetRole(new UserRole { Role = ChannelRole.Moderator, ChannelId = _channelId, ChannelName = _channelInfo.BroadcasterName, }) ); await Task.WhenAll(editorsTasks); await Task.WhenAll(modsTasks); _channelInfo = await channelInfoTask; await RegisterEventSubSubscriptions(CancellationToken.None); }
public override async Task OnActivateAsync() { _channelId = this.GetPrimaryKeyString(); _logger.LogInformation("Activating channel grain {channelId}", _channelId); if (!_channelBotState.RecordExists) { var defaultBotInfo = new BotAccountInfo { IsActive = true, UserId = _options.TokenInfo.UserId, UserLogin = _options.TokenInfo.Login, }; _channelBotState.State.AllowedBotAccounts.Add(defaultBotInfo); _channelBotState.State.Commands = new Dictionary <Guid, Conceptoire.Twitch.Commands.CommandOptions>(); } else { // Temps: fix existing ids foreach ((var id, var command) in _channelBotState.State.Commands) { command.Id = id; if (command.Type == "MessageTracer") { command.Type = "Logger"; } } // Activate bot if it is supposed to be running if (_channelBotState.State.IsActive) { await RegisterEventSubSubscriptions(CancellationToken.None); await StartBot(_channelState.State.BroadcasterToken); } } await base.OnActivateAsync(); _channelInfo = await _appClient.GetChannelInfoAsync(_channelId); await OnChannelUpdate(_channelInfo); }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlipBloopWeb", Version = "v1" }); }); services.AddApplicationInsightsTelemetry(); services.AddTransient <IClientBuilder>(services => { var builder = new ClientBuilder() // Clustering information .Configure <ClusterOptions>(options => { options.ClusterId = "dev"; options.ServiceId = "TwitchServices"; }) .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IChannelGrain).Assembly)); var redisClusteringUrl = Configuration.GetValue <string>("REDIS_URL"); if (!string.IsNullOrEmpty(redisClusteringUrl)) { builder.UseRedisClustering(redisClusteringUrl); } else { builder.UseLocalhostClustering(); } return(builder); }); services.AddSingleton <IClientProvider, GrainClientProvider>(); // Load config, mainly the EventSub secret used for registration services.Configure <EventSubOptions>(Configuration.GetSection("twitch:EventSub")); // Add the EventSub handler instance to DI services.AddEventSub(); // Register an EventSub event Handler for channel.update events services.AddEventSubHandler <TwitchEventSubChannelUpdateEvent>(async(context, eventSub) => { context.Logger.LogInformation("Received a channel update for {channelName}, streaming {category} - {text}", eventSub.BroadcasterUserName, eventSub.CategoryName, eventSub.Title); var grainClientProvider = context.Services.GetRequiredService <IClientProvider>(); var grainClient = await grainClientProvider.GetConnectedClient(); var helixInfo = new HelixChannelInfo { BroadcasterId = eventSub.BroadcasterUserId, BroadcasterName = eventSub.BroadcasterUserName, BroadcasterLanguage = eventSub.Language, GameId = eventSub.CategoryId, GameName = eventSub.CategoryName, Title = eventSub.Title, }; var channelGrain = grainClient.GetGrain <IChannelGrain>(helixInfo.BroadcasterId); await channelGrain.OnChannelUpdate(helixInfo); }); var twitchOptions = Configuration.GetSection("twitch").Get <TwitchApplicationOptions>(); services.AddTransient <IAuthenticated>(services => Twitch.Authenticate() .FromAppCredentials(twitchOptions.ClientId, twitchOptions.ClientSecret) .Build()); services.AddSingleton <TwitchAPIClient>(); services.Configure <AzureGameLocalizationStoreOptions>(Configuration.GetSection("loc:azure")); services.AddSingleton <IGameLocalizationStore, AzureStorageGameLocalizationStore>(); services.AddHttpClient(); Action <TwitchConstants.TwitchOAuthScopes[], string, OpenIdConnectOptions> configureTwitchOpenId = (scopes, callbackPath, options) => { options.Authority = "https://id.twitch.tv/oauth2"; //options.MetadataAddress = "https://id.twitch.tv/oauth2/.well-known/openid-configuration"; options.ClientId = twitchOptions.ClientId; options.ClientSecret = twitchOptions.ClientSecret; options.ResponseType = "token id_token"; options.ResponseMode = "form_post"; options.CallbackPath = callbackPath; options.Events.OnRedirectToIdentityProvider = (context) => { context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri + "-fragment"; return(Task.CompletedTask); }; options.Events.OnTokenValidated = (context) => { ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity; identity.AddClaim(new Claim("access_token", context.ProtocolMessage.AccessToken)); identity.AddClaim(new Claim("id_token", context.ProtocolMessage.IdToken)); foreach (var scope in scopes) { identity.AddClaim(new Claim("scope", TwitchConstants.ScopesValues[scope])); } return(Task.CompletedTask); }; options.Scope.Remove("profile"); foreach (var scope in scopes) { options.Scope.Add(TwitchConstants.ScopesValues[scope]); } }; // Identity services .AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "twitch"; }) .AddCookie("cookie", options => { options.Events.OnRedirectToLogin = context => { if (context.Request.Path.StartsWithSegments("/api")) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; } else { context.Response.Redirect(context.RedirectUri); } return(Task.CompletedTask); }; }) .AddOpenIdConnect("twitch", "Twitch", options => configureTwitchOpenId(new TwitchConstants.TwitchOAuthScopes[] { TwitchConstants.TwitchOAuthScopes.ChannelReadEditors, TwitchConstants.TwitchOAuthScopes.ModerationRead, TwitchConstants.TwitchOAuthScopes.UserReadBlockedUsers, TwitchConstants.TwitchOAuthScopes.UserReadBroadcast, TwitchConstants.TwitchOAuthScopes.UserReadSubscriptions, TwitchConstants.TwitchOAuthScopes.ChatRead, }, "/signin-oidc", options)) .AddOpenIdConnect("twitchBot", "Twitch - Bot Account", options => configureTwitchOpenId(new TwitchConstants.TwitchOAuthScopes[] { TwitchConstants.TwitchOAuthScopes.ChannelReadEditors, TwitchConstants.TwitchOAuthScopes.ModerationRead, TwitchConstants.TwitchOAuthScopes.UserReadBlockedUsers, TwitchConstants.TwitchOAuthScopes.UserReadBroadcast, TwitchConstants.TwitchOAuthScopes.ChatRead, TwitchConstants.TwitchOAuthScopes.ChatEdit, TwitchConstants.TwitchOAuthScopes.UserEditFollows, }, "/signin-oidc-bot", options)); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddMudServices(); services.AddAuthorizationCore(); }