diff --git a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs index ed7896e82..15807146f 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs @@ -28,6 +28,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor private readonly IPubSub _pubSub; private readonly IEmbedBuilderService _eb; + private readonly SearchesConfigService _config; public TypedKey> StreamsOnlineKey { get; } public TypedKey> StreamsOfflineKey { get; } @@ -49,14 +50,16 @@ public sealed class StreamNotificationService : INService, IReadyExecutor IHttpClientFactory httpFactory, Bot bot, IPubSub pubSub, - IEmbedBuilderService eb) + IEmbedBuilderService eb, + SearchesConfigService config) { _db = db; _client = client; _strings = strings; _pubSub = pubSub; _eb = eb; - + _config = config; + _streamTracker = new(httpFactory, creds); StreamsOnlineKey = new("streams.online"); @@ -69,34 +72,34 @@ public sealed class StreamNotificationService : INService, IReadyExecutor { var ids = client.GetGuildIds(); var guildConfigs = uow.Set() - .AsQueryable() - .Include(x => x.FollowedStreams) - .Where(x => ids.Contains(x.GuildId)) - .ToList(); + .AsQueryable() + .Include(x => x.FollowedStreams) + .Where(x => ids.Contains(x.GuildId)) + .ToList(); _offlineNotificationServers = new(guildConfigs - .Where(gc => gc.NotifyStreamOffline) - .Select(x => x.GuildId) - .ToList()); - + .Where(gc => gc.NotifyStreamOffline) + .Select(x => x.GuildId) + .ToList()); + _deleteOnOfflineServers = new(guildConfigs - .Where(gc => gc.DeleteStreamOnlineMessage) - .Select(x => x.GuildId) - .ToList()); + .Where(gc => gc.DeleteStreamOnlineMessage) + .Select(x => x.GuildId) + .ToList()); var followedStreams = guildConfigs.SelectMany(x => x.FollowedStreams).ToList(); _shardTrackedStreams = followedStreams.GroupBy(x => new - { - x.Type, - Name = x.Username.ToLower() - }) - .ToList() - .ToDictionary( - x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()), - x => x.GroupBy(y => y.GuildId) - .ToDictionary(y => y.Key, - y => y.AsEnumerable().ToHashSet())); + { + x.Type, + Name = x.Username.ToLower() + }) + .ToList() + .ToDictionary( + x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()), + x => x.GroupBy(y => y.GuildId) + .ToDictionary(y => y.Key, + y => y.AsEnumerable().ToHashSet())); // shard 0 will keep track of when there are no more guilds which track a stream if (client.ShardId == 0) @@ -107,12 +110,12 @@ public sealed class StreamNotificationService : INService, IReadyExecutor _streamTracker.AddLastData(fs.CreateKey(), null, false); _trackCounter = allFollowedStreams.GroupBy(x => new - { - x.Type, - Name = x.Username.ToLower() - }) - .ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name), - x => x.Select(fs => fs.GuildId).ToHashSet()); + { + x.Type, + Name = x.Username.ToLower() + }) + .ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name), + x => x.Select(fs => fs.GuildId).ToHashSet()); } } @@ -152,7 +155,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor continue; var deleteGroups = failingStreams.GroupBy(x => x.Type) - .ToDictionary(x => x.Key, x => x.Select(y => y.Name).ToList()); + .ToDictionary(x => x.Key, x => x.Select(y => y.Name).ToList()); await using var uow = _db.GetDbContext(); foreach (var kvp in deleteGroups) @@ -165,9 +168,9 @@ public sealed class StreamNotificationService : INService, IReadyExecutor string.Join(", ", kvp.Value)); var toDelete = uow.Set() - .AsQueryable() - .Where(x => x.Type == kvp.Key && kvp.Value.Contains(x.Username)) - .ToList(); + .AsQueryable() + .Where(x => x.Type == kvp.Key && kvp.Value.Contains(x.Username)) + .ToList(); uow.RemoveRange(toDelete); await uow.SaveChangesAsync(); @@ -246,17 +249,17 @@ public sealed class StreamNotificationService : INService, IReadyExecutor if (_shardTrackedStreams.TryGetValue(key, out var fss)) { await fss - // send offline stream notifications only to guilds which enable it with .stoff - .SelectMany(x => x.Value) - .Where(x => _offlineNotificationServers.Contains(x.GuildId)) - .Select(fs => _client.GetGuild(fs.GuildId) - ?.GetTextChannel(fs.ChannelId) - ?.EmbedAsync(GetEmbed(fs.GuildId, stream))) - .WhenAll(); + // send offline stream notifications only to guilds which enable it with .stoff + .SelectMany(x => x.Value) + .Where(x => _offlineNotificationServers.Contains(x.GuildId)) + .Select(fs => _client.GetGuild(fs.GuildId) + ?.GetTextChannel(fs.ChannelId) + ?.EmbedAsync(GetEmbed(fs.GuildId, stream))) + .WhenAll(); } } } - + private async ValueTask HandleStreamsOnline(List onlineStreams) { @@ -266,30 +269,30 @@ public sealed class StreamNotificationService : INService, IReadyExecutor if (_shardTrackedStreams.TryGetValue(key, out var fss)) { var messages = await fss.SelectMany(x => x.Value) - .Select(async fs => - { - var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId); + .Select(async fs => + { + var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId); - if (textChannel is null) - return default; + if (textChannel is null) + return default; - var rep = new ReplacementBuilder().WithOverride("%user%", () => fs.Username) - .WithOverride("%platform%", () => fs.Type.ToString()) - .Build(); + var rep = new ReplacementBuilder().WithOverride("%user%", () => fs.Username) + .WithOverride("%platform%", () => fs.Type.ToString()) + .Build(); - var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message); + var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message); - var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream, false), message); + var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream, false), message); + + // only cache the ids of channel/message pairs + if (_deleteOnOfflineServers.Contains(fs.GuildId)) + return (textChannel.Id, msg.Id); + else + return default; + }) + .WhenAll(); - // only cache the ids of channel/message pairs - if(_deleteOnOfflineServers.Contains(fs.GuildId)) - return (textChannel.Id, msg.Id); - else - return default; - }) - .WhenAll(); - // push online stream messages to redis // when streams go offline, any server which // has the online stream message deletion feature @@ -297,16 +300,15 @@ public sealed class StreamNotificationService : INService, IReadyExecutor try { var pairs = messages - .Where(x => x != default) - .Select(x => (x.Item1, x.Item2)) - .ToList(); + .Where(x => x != default) + .Select(x => (x.Item1, x.Item2)) + .ToList(); if (pairs.Count > 0) await OnlineMessagesSent(key.Type, key.Name, pairs); } catch { - } } } @@ -384,10 +386,10 @@ public sealed class StreamNotificationService : INService, IReadyExecutor await using (var uow = _db.GetDbContext()) { var fss = uow.Set() - .AsQueryable() - .Where(x => x.GuildId == guildId) - .OrderBy(x => x.Id) - .ToList(); + .AsQueryable() + .Where(x => x.GuildId == guildId) + .OrderBy(x => x.Id) + .ToList(); // out of range if (fss.Count <= index) @@ -450,7 +452,9 @@ public sealed class StreamNotificationService : INService, IReadyExecutor GuildId = guildId }; - if (gc.FollowedStreams.Count >= 10) + var config = _config.Data; + if (config.FollowedStreams.MaxCount is not -1 + && gc.FollowedStreams.Count >= config.FollowedStreams.MaxCount) return null; gc.FollowedStreams.Add(fs); @@ -475,10 +479,10 @@ public sealed class StreamNotificationService : INService, IReadyExecutor public IEmbedBuilder GetEmbed(ulong guildId, StreamData status, bool showViewers = true) { var embed = _eb.Create() - .WithTitle(status.Name) - .WithUrl(status.StreamUrl) - .WithDescription(status.StreamUrl) - .AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true); + .WithTitle(status.Name) + .WithUrl(status.StreamUrl) + .WithDescription(status.StreamUrl) + .AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true); if (showViewers) { @@ -527,7 +531,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor return newValue; } - + public bool ToggleStreamOnlineDelete(ulong guildId) { using var uow = _db.GetDbContext(); diff --git a/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs index f2147e547..4bab0d8b8 100644 --- a/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs +++ b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs @@ -8,18 +8,18 @@ public partial class SearchesConfig : ICloneable { [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. @@ -55,6 +55,15 @@ 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 InvidiousInstances { get; set; } = new List(); + + [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 diff --git a/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs index 4d58098b4..87de6ec0e 100644 --- a/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs +++ b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs @@ -17,17 +17,22 @@ public class SearchesConfigService : ConfigServiceBase 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, + ConfigParsers.InsensitiveEnum, + ConfigPrinters.ToString); + Migrate(); } @@ -41,5 +46,13 @@ public class SearchesConfigService : ConfigServiceBase c.WebSearchEngine = WebSearchEngine.Google_Scrape; }); } + + if (data.Version < 2) + { + ModifyConfig(c => + { + c.Version = 2; + }); + } } } \ No newline at end of file