diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 792e1ca0..1284d71b 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -7,82 +7,54 @@ using System.Linq; using System.Threading.Tasks; using NadekoBot.Services; using NadekoBot.Attributes; -using Discord.WebSocket; using NadekoBot.Services.Database.Models; -using static NadekoBot.Modules.Permissions.Permissions; -using System.Collections.Concurrent; -using NLog; -using NadekoBot.Modules.Permissions; +using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { - [NadekoModule("Administration", ".")] public partial class Administration : NadekoTopLevelModule { - private static readonly ConcurrentHashSet deleteMessagesOnCommand; + private IGuild _nadekoSupportServer; + private readonly DbHandler _db; + private readonly AdministrationService _admin; - private new static readonly Logger _log; - - static Administration() + public Administration(DbHandler db, AdministrationService admin) { - _log = LogManager.GetCurrentClassLogger(); - NadekoBot.CommandHandler.CommandExecuted += DelMsgOnCmd_Handler; - - deleteMessagesOnCommand = new ConcurrentHashSet(NadekoBot.AllGuildConfigs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId)); - + _db = db; + _admin = admin; } - private static Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd) - { - var _ = Task.Run(async () => - { - try - { - var channel = msg.Channel as SocketTextChannel; - if (channel == null) - return; - if (deleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick") - await msg.DeleteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn(ex, "Delmsgoncmd errored..."); - } - }); - return Task.CompletedTask; - } + ////todo permissions + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[RequireUserPermission(GuildPermission.Administrator)] + //public async Task ResetPermissions() + //{ + // using (var uow = _db.UnitOfWork) + // { + // var config = uow.GuildConfigs.GcWithPermissionsv2For(Context.Guild.Id); + // config.Permissions = Permissionv2.GetDefaultPermlist; + // await uow.CompleteAsync(); + // UpdateCache(config); + // } + // await ReplyConfirmLocalized("perms_reset").ConfigureAwait(false); + //} + //[NadekoCommand, Usage, Description, Aliases] + //[OwnerOnly] + //public async Task ResetGlobalPermissions() + //{ + // using (var uow = _db.UnitOfWork) + // { + // var gc = uow.BotConfig.GetOrCreate(); + // gc.BlockedCommands.Clear(); + // gc.BlockedModules.Clear(); - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.Administrator)] - public async Task ResetPermissions() - { - using (var uow = DbHandler.UnitOfWork()) - { - var config = uow.GuildConfigs.GcWithPermissionsv2For(Context.Guild.Id); - config.Permissions = Permissionv2.GetDefaultPermlist; - await uow.CompleteAsync(); - UpdateCache(config); - } - await ReplyConfirmLocalized("perms_reset").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [OwnerOnly] - public async Task ResetGlobalPermissions() - { - using (var uow = DbHandler.UnitOfWork()) - { - var gc = uow.BotConfig.GetOrCreate(); - gc.BlockedCommands.Clear(); - gc.BlockedModules.Clear(); - - GlobalPermissionCommands.BlockedCommands.Clear(); - GlobalPermissionCommands.BlockedModules.Clear(); - await uow.CompleteAsync(); - } - await ReplyConfirmLocalized("global_perms_reset").ConfigureAwait(false); - } + // GlobalPermissionCommands.BlockedCommands.Clear(); + // GlobalPermissionCommands.BlockedModules.Clear(); + // await uow.CompleteAsync(); + // } + // await ReplyConfirmLocalized("global_perms_reset").ConfigureAwait(false); + //} [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -91,7 +63,7 @@ namespace NadekoBot.Modules.Administration public async Task Delmsgoncmd() { bool enabled; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var conf = uow.GuildConfigs.For(Context.Guild.Id, set => set); enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand; @@ -100,12 +72,12 @@ namespace NadekoBot.Modules.Administration } if (enabled) { - deleteMessagesOnCommand.Add(Context.Guild.Id); + _admin.DeleteMessagesOnCommand.Add(Context.Guild.Id); await ReplyConfirmLocalized("delmsg_on").ConfigureAwait(false); } else { - deleteMessagesOnCommand.TryRemove(Context.Guild.Id); + _admin.DeleteMessagesOnCommand.TryRemove(Context.Guild.Id); await ReplyConfirmLocalized("delmsg_off").ConfigureAwait(false); } } @@ -454,19 +426,18 @@ namespace NadekoBot.Modules.Administration await Context.Channel.SendMessageAsync(send).ConfigureAwait(false); } - private IGuild _nadekoSupportServer; [NadekoCommand, Usage, Description, Aliases] public async Task Donators() { IEnumerable donatorsOrdered; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { donatorsOrdered = uow.Donators.GetDonatorsOrdered(); } await Context.Channel.SendConfirmAsync(GetText("donators"), string.Join("⭐", donatorsOrdered.Select(d => d.Name))).ConfigureAwait(false); - _nadekoSupportServer = _nadekoSupportServer ?? NadekoBot.Client.GetGuild(117523346618318850); + _nadekoSupportServer = _nadekoSupportServer ?? (await Context.Client.GetGuildAsync(117523346618318850)); var patreonRole = _nadekoSupportServer?.GetRole(236667642088259585); if (patreonRole == null) @@ -482,7 +453,7 @@ namespace NadekoBot.Modules.Administration public async Task Donadd(IUser donator, int amount) { Donator don; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { don = uow.Donators.AddOrUpdateDonator(donator.Id, donator.Username, amount); await uow.CompleteAsync(); diff --git a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs b/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs index f4dad471..86345ab4 100644 --- a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs @@ -4,6 +4,7 @@ using Discord.WebSocket; using Microsoft.EntityFrameworkCore; using NadekoBot.Attributes; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using NLog; using System; @@ -19,232 +20,13 @@ namespace NadekoBot.Modules.Administration [Group] public class MuteCommands : NadekoSubmodule { - private static ConcurrentDictionary GuildMuteRoles { get; } - private static ConcurrentDictionary> MutedUsers { get; } - private static ConcurrentDictionary> UnmuteTimers { get; } - = new ConcurrentDictionary>(); + private readonly MuteService _service; + private readonly DbHandler _db; - public static event Action UserMuted = delegate { }; - public static event Action UserUnmuted = delegate { }; - - - public enum MuteType { - Voice, - Chat, - All - } - private static readonly OverwritePermissions denyOverwrite = new OverwritePermissions(sendMessages: PermValue.Deny, attachFiles: PermValue.Deny); - - private static readonly new Logger _log = LogManager.GetCurrentClassLogger(); - - static MuteCommands() + public MuteCommands(MuteService service, DbHandler db) { - var configs = NadekoBot.AllGuildConfigs; - GuildMuteRoles = new ConcurrentDictionary(configs - .Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName)) - .ToDictionary(c => c.GuildId, c => c.MuteRoleName)); - - MutedUsers = new ConcurrentDictionary>(configs.ToDictionary( - k => k.GuildId, - v => new ConcurrentHashSet(v.MutedUsers.Select(m => m.UserId)) - )); - - foreach (var conf in configs) - { - foreach (var x in conf.UnmuteTimers) - { - TimeSpan after; - if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - { - after = TimeSpan.FromMinutes(2); - } - else - { - after = x.UnmuteAt - DateTime.UtcNow; - } - StartUnmuteTimer(conf.GuildId, x.UserId, after); - } - } - - NadekoBot.Client.UserJoined += Client_UserJoined; - } - - private static async Task Client_UserJoined(IGuildUser usr) - { - try - { - MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted); - - if (muted == null || !muted.Contains(usr.Id)) - return; - await MuteUser(usr).ConfigureAwait(false); - } - catch (Exception ex) - { - LogManager.GetCurrentClassLogger().Warn(ex); - } - - } - - public static async Task MuteUser(IGuildUser usr) - { - await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); - var muteRole = await GetMuteRole(usr.Guild); - if (!usr.RoleIds.Contains(muteRole.Id)) - await usr.AddRoleAsync(muteRole).ConfigureAwait(false); - StopUnmuteTimer(usr.GuildId, usr.Id); - using (var uow = DbHandler.UnitOfWork()) - { - var config = uow.GuildConfigs.For(usr.Guild.Id, - set => set.Include(gc => gc.MutedUsers) - .Include(gc => gc.UnmuteTimers)); - config.MutedUsers.Add(new MutedUserId() - { - UserId = usr.Id - }); - if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted)) - muted.Add(usr.Id); - - config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); - - await uow.CompleteAsync().ConfigureAwait(false); - } - UserMuted(usr, MuteType.All); - } - - public static async Task UnmuteUser(IGuildUser usr) - { - StopUnmuteTimer(usr.GuildId, usr.Id); - try { await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); } catch { } - try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)).ConfigureAwait(false); } catch { /*ignore*/ } - using (var uow = DbHandler.UnitOfWork()) - { - var config = uow.GuildConfigs.For(usr.Guild.Id, set => set.Include(gc => gc.MutedUsers) - .Include(gc => gc.UnmuteTimers)); - config.MutedUsers.Remove(new MutedUserId() - { - UserId = usr.Id - }); - if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted)) - muted.TryRemove(usr.Id); - - config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); - - await uow.CompleteAsync().ConfigureAwait(false); - } - UserUnmuted(usr, MuteType.All); - } - - public static async TaskGetMuteRole(IGuild guild) - { - const string defaultMuteRoleName = "nadeko-mute"; - - var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName); - - var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName); - if (muteRole == null) - { - - //if it doesn't exist, create it - try { muteRole = await guild.CreateRoleAsync(muteRoleName, GuildPermissions.None).ConfigureAwait(false); } - catch - { - //if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one - muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) ?? - await guild.CreateRoleAsync(defaultMuteRoleName, GuildPermissions.None).ConfigureAwait(false); - } - } - - foreach (var toOverwrite in (await guild.GetTextChannelsAsync())) - { - try - { - if (!toOverwrite.PermissionOverwrites.Select(x => x.Permissions).Contains(denyOverwrite)) - { - await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite) - .ConfigureAwait(false); - - await Task.Delay(200).ConfigureAwait(false); - } - } - catch - { - // ignored - } - } - - return muteRole; - } - - public static async Task TimedMute(IGuildUser user, TimeSpan after) - { - await MuteUser(user).ConfigureAwait(false); // mute the user. This will also remove any previous unmute timers - using (var uow = DbHandler.UnitOfWork()) - { - var config = uow.GuildConfigs.For(user.GuildId, set => set.Include(x => x.UnmuteTimers)); - config.UnmuteTimers.Add(new UnmuteTimer() - { - UserId = user.Id, - UnmuteAt = DateTime.UtcNow + after, - }); // add teh unmute timer to the database - uow.Complete(); - } - StartUnmuteTimer(user.GuildId, user.Id, after); // start the timer - } - - public static void StartUnmuteTimer(ulong guildId, ulong userId, TimeSpan after) - { - //load the unmute timers for this guild - var userUnmuteTimers = UnmuteTimers.GetOrAdd(guildId, new ConcurrentDictionary()); - - //unmute timer to be added - var toAdd = new Timer(async _ => - { - try - { - var guild = NadekoBot.Client.GetGuild(guildId); // load the guild - if (guild == null) - { - RemoveUnmuteTimerFromDb(guildId, userId); - return; // if guild can't be found, just remove the timer from db - } - // unmute the user, this will also remove the timer from the db - await UnmuteUser(guild.GetUser(userId)).ConfigureAwait(false); - } - catch (Exception ex) - { - RemoveUnmuteTimerFromDb(guildId, userId); // if unmute errored, just remove unmute from db - Administration._log.Warn("Couldn't unmute user {0} in guild {1}", userId, guildId); - Administration._log.Warn(ex); - } - }, null, after, Timeout.InfiniteTimeSpan); - - //add it, or stop the old one and add this one - userUnmuteTimers.AddOrUpdate(userId, (key) => toAdd, (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return toAdd; - }); - } - - public static void StopUnmuteTimer(ulong guildId, ulong userId) - { - if (!UnmuteTimers.TryGetValue(guildId, out ConcurrentDictionary userUnmuteTimers)) return; - - if (userUnmuteTimers.TryRemove(userId, out Timer removed)) - { - removed.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - private static void RemoveUnmuteTimerFromDb(ulong guildId, ulong userId) - { - using (var uow = DbHandler.UnitOfWork()) - { - var config = uow.GuildConfigs.For(guildId, set => set.Include(x => x.UnmuteTimers)); - config.UnmuteTimers.RemoveWhere(x => x.UserId == userId); - uow.Complete(); - } + _service = service; + _db = db; } [NadekoCommand, Usage, Description, Aliases] @@ -257,11 +39,11 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(name)) return; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.GuildConfigs.For(Context.Guild.Id, set => set); config.MuteRoleName = name; - GuildMuteRoles.AddOrUpdate(Context.Guild.Id, name, (id, old) => name); + _service.GuildMuteRoles.AddOrUpdate(Context.Guild.Id, name, (id, old) => name); await uow.CompleteAsync().ConfigureAwait(false); } await ReplyConfirmLocalized("mute_role_set").ConfigureAwait(false); @@ -283,7 +65,7 @@ namespace NadekoBot.Modules.Administration { try { - await MuteUser(user).ConfigureAwait(false); + await _service.MuteUser(user).ConfigureAwait(false); await ReplyConfirmLocalized("user_muted", Format.Bold(user.ToString())).ConfigureAwait(false); } catch @@ -303,7 +85,7 @@ namespace NadekoBot.Modules.Administration return; try { - await TimedMute(user, TimeSpan.FromMinutes(minutes)).ConfigureAwait(false); + await _service.TimedMute(user, TimeSpan.FromMinutes(minutes)).ConfigureAwait(false); await ReplyConfirmLocalized("user_muted_time", Format.Bold(user.ToString()), minutes).ConfigureAwait(false); } catch (Exception ex) @@ -321,7 +103,7 @@ namespace NadekoBot.Modules.Administration { try { - await UnmuteUser(user).ConfigureAwait(false); + await _service.UnmuteUser(user).ConfigureAwait(false); await ReplyConfirmLocalized("user_unmuted", Format.Bold(user.ToString())).ConfigureAwait(false); } catch @@ -337,8 +119,7 @@ namespace NadekoBot.Modules.Administration { try { - await user.AddRoleAsync(await GetMuteRole(Context.Guild).ConfigureAwait(false)).ConfigureAwait(false); - UserMuted(user, MuteType.Chat); + await _service.MuteUser(user, MuteType.Chat).ConfigureAwait(false); await ReplyConfirmLocalized("user_chat_mute", Format.Bold(user.ToString())).ConfigureAwait(false); } catch @@ -354,8 +135,7 @@ namespace NadekoBot.Modules.Administration { try { - await user.RemoveRoleAsync(await GetMuteRole(Context.Guild).ConfigureAwait(false)).ConfigureAwait(false); - UserUnmuted(user, MuteType.Chat); + await _service.UnmuteUser(user, MuteType.Chat).ConfigureAwait(false); await ReplyConfirmLocalized("user_chat_unmute", Format.Bold(user.ToString())).ConfigureAwait(false); } catch @@ -367,12 +147,11 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.MuteMembers)] - public async Task VoiceMute(IGuildUser user) + public async Task VoiceMute([Remainder] IGuildUser user) { try { - await user.ModifyAsync(usr => usr.Mute = true).ConfigureAwait(false); - UserMuted(user, MuteType.Voice); + await _service.MuteUser(user, MuteType.Voice).ConfigureAwait(false); await ReplyConfirmLocalized("user_voice_mute", Format.Bold(user.ToString())).ConfigureAwait(false); } catch @@ -384,12 +163,11 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.MuteMembers)] - public async Task VoiceUnmute(IGuildUser user) + public async Task VoiceUnmute([Remainder] IGuildUser user) { try { - await user.ModifyAsync(usr => usr.Mute = false).ConfigureAwait(false); - UserUnmuted(user, MuteType.Voice); + await _service.UnmuteUser(user, MuteType.Voice).ConfigureAwait(false); await ReplyConfirmLocalized("user_voice_unmute", Format.Bold(user.ToString())).ConfigureAwait(false); } catch diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index a7ac2f46..d33fdeda 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -15,6 +15,7 @@ using Microsoft.EntityFrameworkCore; using System.Collections.Immutable; using NadekoBot.DataStructures; using NLog; +using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { @@ -23,34 +24,20 @@ namespace NadekoBot.Modules.Administration [Group] public class SelfCommands : NadekoSubmodule { - private static volatile bool _forwardDMs; - private static volatile bool _forwardDMsToAllOwners; + private readonly DbHandler _db; private static readonly object _locker = new object(); + private readonly SelfService _service; + private readonly DiscordShardedClient _client; + private readonly IImagesService _images; - private new static readonly Logger _log; - - static SelfCommands() + public SelfCommands(DbHandler db, SelfService service, DiscordShardedClient client, + IImagesService images) { - _log = LogManager.GetCurrentClassLogger(); - using (var uow = DbHandler.UnitOfWork()) - { - var config = uow.BotConfig.GetOrCreate(); - _forwardDMs = config.ForwardMessages; - _forwardDMsToAllOwners = config.ForwardToAllOwners; - } - - var _ = Task.Run(async () => - { - while (!NadekoBot.Ready) - await Task.Delay(1000); - - foreach (var cmd in NadekoBot.BotConfig.StartupCommands) - { - await NadekoBot.CommandHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText); - await Task.Delay(400).ConfigureAwait(false); - } - }); + _db = db; + _service = service; + _client = client; + _images = images; } [NadekoCommand, Usage, Description, Aliases] @@ -69,7 +56,7 @@ namespace NadekoBot.Modules.Administration VoiceChannelId = guser.VoiceChannel?.Id, VoiceChannelName = guser.VoiceChannel?.Name, }; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.BotConfig .GetOrCreate(set => set.Include(x => x.StartupCommands)) @@ -96,7 +83,7 @@ namespace NadekoBot.Modules.Administration return; page -= 1; IEnumerable scmds; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { scmds = uow.BotConfig .GetOrCreate(set => set.Include(x => x.StartupCommands)) @@ -148,7 +135,7 @@ namespace NadekoBot.Modules.Administration public async Task StartupCommandRemove([Remainder] string cmdText) { StartupCommand cmd; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var cmds = uow.BotConfig .GetOrCreate(set => set.Include(x => x.StartupCommands)) @@ -174,7 +161,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task StartupCommandsClear() { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.BotConfig .GetOrCreate(set => set.Include(x => x.StartupCommands)) @@ -190,14 +177,13 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ForwardMessages() { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.BotConfig.GetOrCreate(); - lock (_locker) - _forwardDMs = config.ForwardMessages = !config.ForwardMessages; + _service.ForwardDMs = config.ForwardMessages = !config.ForwardMessages; uow.Complete(); } - if (_forwardDMs) + if (_service.ForwardDMs) await ReplyConfirmLocalized("fwdm_start").ConfigureAwait(false); else await ReplyConfirmLocalized("fwdm_stop").ConfigureAwait(false); @@ -207,83 +193,83 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ForwardToAll() { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.BotConfig.GetOrCreate(); lock (_locker) - _forwardDMsToAllOwners = config.ForwardToAllOwners = !config.ForwardToAllOwners; + _service.ForwardDMsToAllOwners = config.ForwardToAllOwners = !config.ForwardToAllOwners; uow.Complete(); } - if (_forwardDMsToAllOwners) + if (_service.ForwardDMsToAllOwners) await ReplyConfirmLocalized("fwall_start").ConfigureAwait(false); else await ReplyConfirmLocalized("fwall_stop").ConfigureAwait(false); } - public static async Task HandleDmForwarding(IUserMessage msg, ImmutableArray> ownerChannels) - { - if (_forwardDMs && ownerChannels.Length > 0) - { - var title = GetTextStatic("dm_from", - NadekoBot.Localization.DefaultCultureInfo, - typeof(Administration).Name.ToLowerInvariant()) + - $" [{msg.Author}]({msg.Author.Id})"; + //todo dm forwarding + //public async Task HandleDmForwarding(IUserMessage msg, ImmutableArray> ownerChannels) + //{ + // if (_service.ForwardDMs && ownerChannels.Length > 0) + // { + // var title = _strings.GetText("dm_from", + // NadekoBot.Localization.DefaultCultureInfo, + // typeof(Administration).Name.ToLowerInvariant()) + + // $" [{msg.Author}]({msg.Author.Id})"; - var attachamentsTxt = GetTextStatic("attachments", - NadekoBot.Localization.DefaultCultureInfo, - typeof(Administration).Name.ToLowerInvariant()); + // var attachamentsTxt = GetTextStatic("attachments", + // NadekoBot.Localization.DefaultCultureInfo, + // typeof(Administration).Name.ToLowerInvariant()); - var toSend = msg.Content; + // var toSend = msg.Content; - if (msg.Attachments.Count > 0) - { - toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n" + - string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl)); - } - - if (_forwardDMsToAllOwners) - { - var allOwnerChannels = await Task.WhenAll(ownerChannels - .Select(x => x.Value)) - .ConfigureAwait(false); - - foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id)) - { - try - { - await ownerCh.SendConfirmAsync(title, toSend).ConfigureAwait(false); - } - catch - { - _log.Warn("Can't contact owner with id {0}", ownerCh.Recipient.Id); - } - } - } - else - { - var firstOwnerChannel = await ownerChannels[0]; - if (firstOwnerChannel.Recipient.Id != msg.Author.Id) - { - try - { - await firstOwnerChannel.SendConfirmAsync(title, toSend).ConfigureAwait(false); - } - catch - { - // ignored - } - } - } - } - } + // if (msg.Attachments.Count > 0) + // { + // toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n" + + // string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl)); + // } + // if (_service.ForwardDMsToAllOwners) + // { + // var allOwnerChannels = await Task.WhenAll(ownerChannels + // .Select(x => x.Value)) + // .ConfigureAwait(false); + // foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id)) + // { + // try + // { + // await ownerCh.SendConfirmAsync(title, toSend).ConfigureAwait(false); + // } + // catch + // { + // _log.Warn("Can't contact owner with id {0}", ownerCh.Recipient.Id); + // } + // } + // } + // else + // { + // var firstOwnerChannel = await ownerChannels[0]; + // if (firstOwnerChannel.Recipient.Id != msg.Author.Id) + // { + // try + // { + // await firstOwnerChannel.SendConfirmAsync(title, toSend).ConfigureAwait(false); + // } + // catch + // { + // // ignored + // } + // } + // } + // } + // } + [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] public async Task ConnectShard(int shardid) { - var shard = NadekoBot.Client.GetShard(shardid); + var shard = _client.GetShard(shardid); if (shard == null) { @@ -307,15 +293,15 @@ namespace NadekoBot.Modules.Administration public async Task Leave([Remainder] string guildStr) { guildStr = guildStr.Trim().ToUpperInvariant(); - var server = NadekoBot.Client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr) ?? - NadekoBot.Client.Guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == guildStr); + var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr) ?? + _client.Guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == guildStr); if (server == null) { await ReplyErrorLocalized("no_server").ConfigureAwait(false); return; } - if (server.OwnerId != NadekoBot.Client.CurrentUser.Id) + if (server.OwnerId != _client.CurrentUser.Id) { await server.LeaveAsync().ConfigureAwait(false); await ReplyConfirmLocalized("left_server", Format.Bold(server.Name)).ConfigureAwait(false); @@ -351,7 +337,7 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(newName)) return; - await NadekoBot.Client.CurrentUser.ModifyAsync(u => u.Username = newName).ConfigureAwait(false); + await _client.CurrentUser.ModifyAsync(u => u.Username = newName).ConfigureAwait(false); await ReplyConfirmLocalized("bot_name", Format.Bold(newName)).ConfigureAwait(false); } @@ -360,7 +346,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task SetStatus([Remainder] SettableUserStatus status) { - await NadekoBot.Client.SetStatusAsync(SettableUserStatusToUserStatus(status)).ConfigureAwait(false); + await _client.SetStatusAsync(SettableUserStatusToUserStatus(status)).ConfigureAwait(false); await ReplyConfirmLocalized("bot_status", Format.Bold(status.ToString())).ConfigureAwait(false); } @@ -380,7 +366,7 @@ namespace NadekoBot.Modules.Administration await sr.CopyToAsync(imgStream); imgStream.Position = 0; - await NadekoBot.Client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)).ConfigureAwait(false); + await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)).ConfigureAwait(false); } } @@ -391,7 +377,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task SetGame([Remainder] string game = null) { - await NadekoBot.Client.SetGameAsync(game).ConfigureAwait(false); + await _client.SetGameAsync(game).ConfigureAwait(false); await ReplyConfirmLocalized("set_game").ConfigureAwait(false); } @@ -402,7 +388,7 @@ namespace NadekoBot.Modules.Administration { name = name ?? ""; - await NadekoBot.Client.SetGameAsync(name, url, StreamType.Twitch).ConfigureAwait(false); + await _client.SetGameAsync(name, url, StreamType.Twitch).ConfigureAwait(false); await ReplyConfirmLocalized("set_stream").ConfigureAwait(false); } @@ -418,7 +404,7 @@ namespace NadekoBot.Modules.Administration if (ids.Length != 2) return; var sid = ulong.Parse(ids[0]); - var server = NadekoBot.Client.Guilds.FirstOrDefault(s => s.Id == sid); + var server = _client.Guilds.FirstOrDefault(s => s.Id == sid); if (server == null) return; @@ -455,7 +441,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task Announce([Remainder] string message) { - var channels = NadekoBot.Client.Guilds.Select(g => g.DefaultChannel).ToArray(); + var channels = _client.Guilds.Select(g => g.DefaultChannel).ToArray(); if (channels == null) return; await Task.WhenAll(channels.Where(c => c != null).Select(c => c.SendConfirmAsync(GetText("message_from_bo", Context.User.ToString()), message))) @@ -468,7 +454,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ReloadImages() { - var time = await NadekoBot.Images.Reload().ConfigureAwait(false); + var time = _images.Reload(); await ReplyConfirmLocalized("images_loaded", time.TotalSeconds.ToString("F3")).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs b/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs index 8cfee39c..1d05f533 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs @@ -1,14 +1,9 @@ using Discord; using Discord.Commands; using NadekoBot.Attributes; -using NadekoBot.DataStructures; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NLog; -using System; -using System.Collections.Concurrent; -using System.Linq; using System.Threading.Tasks; namespace NadekoBot.Modules.Administration @@ -18,158 +13,13 @@ namespace NadekoBot.Modules.Administration [Group] public class ServerGreetCommands : NadekoSubmodule { - //make this to a field in the guildconfig table + private readonly GreetSettingsService _greetService; + private readonly DbHandler _db; - private new static Logger _log { get; } - private static readonly GreetSettingsService greetService; - - static ServerGreetCommands() + public ServerGreetCommands(GreetSettingsService greetService, DbHandler db) { - NadekoBot.Client.UserJoined += UserJoined; - NadekoBot.Client.UserLeft += UserLeft; - _log = LogManager.GetCurrentClassLogger(); - - //todo di - greetService = NadekoBot.GreetSettingsService; - } - - private static Task UserLeft(IGuildUser user) - { - var _ = Task.Run(async () => - { - try - { - var conf = greetService.GetOrAddSettingsForGuild(user.GuildId); - - if (!conf.SendChannelByeMessage) return; - var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.ByeMessageChannelId); - - if (channel == null) //maybe warn the server owner that the channel is missing - return; - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelByeMessageText, out embedData)) - { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - try - { - var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); - if (conf.AutoDeleteByeMessagesTimer > 0) - { - toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); - } - } - catch (Exception ex) { _log.Warn(ex); } - } - else - { - var msg = conf.ChannelByeMessageText.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - if (string.IsNullOrWhiteSpace(msg)) - return; - try - { - var toDelete = await channel.SendMessageAsync(msg.SanitizeMentions()).ConfigureAwait(false); - if (conf.AutoDeleteByeMessagesTimer > 0) - { - toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); - } - } - catch (Exception ex) { _log.Warn(ex); } - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private static Task UserJoined(IGuildUser user) - { - var _ = Task.Run(async () => - { - try - { - var conf = greetService.GetOrAddSettingsForGuild(user.GuildId); - - if (conf.SendChannelGreetMessage) - { - var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.GreetMessageChannelId); - if (channel != null) //maybe warn the server owner that the channel is missing - { - - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData)) - { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - try - { - var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); - if (conf.AutoDeleteGreetMessagesTimer > 0) - { - toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); - } - } - catch (Exception ex) { _log.Warn(ex); } - } - else - { - var msg = conf.ChannelGreetMessageText.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - if (!string.IsNullOrWhiteSpace(msg)) - { - try - { - var toDelete = await channel.SendMessageAsync(msg.SanitizeMentions()).ConfigureAwait(false); - if (conf.AutoDeleteGreetMessagesTimer > 0) - { - toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); - } - } - catch (Exception ex) { _log.Warn(ex); } - } - } - } - } - - if (conf.SendDmGreetMessage) - { - var channel = await user.CreateDMChannelAsync(); - - if (channel != null) - { - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData)) - { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - try - { - await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex); } - } - else - { - var msg = conf.DmGreetMessageText.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - if (!string.IsNullOrWhiteSpace(msg)) - { - await channel.SendConfirmAsync(msg).ConfigureAwait(false); - } - } - } - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; + _greetService = greetService; + _db = db; } [NadekoCommand, Usage, Description, Aliases] @@ -180,7 +30,7 @@ namespace NadekoBot.Modules.Administration if (timer < 0 || timer > 600) return; - await ServerGreetCommands.SetGreetDel(Context.Guild.Id, timer).ConfigureAwait(false); + await _greetService.SetGreetDel(Context.Guild.Id, timer).ConfigureAwait(false); if (timer > 0) await ReplyConfirmLocalized("greetdel_on", timer).ConfigureAwait(false); @@ -188,29 +38,12 @@ namespace NadekoBot.Modules.Administration await ReplyConfirmLocalized("greetdel_off").ConfigureAwait(false); } - private static async Task SetGreetDel(ulong id, int timer) - { - if (timer < 0 || timer > 600) - return; - - using (var uow = DbHandler.UnitOfWork()) - { - var conf = uow.GuildConfigs.For(id, set => set); - conf.AutoDeleteGreetMessagesTimer = timer; - - var toAdd = GreetSettings.Create(conf); - greetService.GuildConfigsCache.AddOrUpdate(id, toAdd, (key, old) => toAdd); - - await uow.CompleteAsync().ConfigureAwait(false); - } - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageGuild)] public async Task Greet() { - var enabled = await greetService.SetGreet(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); + var enabled = await _greetService.SetGreet(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); if (enabled) await ReplyConfirmLocalized("greet_on").ConfigureAwait(false); @@ -226,7 +59,7 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(text)) { string channelGreetMessageText; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { channelGreetMessageText = uow.GuildConfigs.For(Context.Guild.Id, set => set).ChannelGreetMessageText; } @@ -234,7 +67,7 @@ namespace NadekoBot.Modules.Administration return; } - var sendGreetEnabled = greetService.SetGreetMessage(Context.Guild.Id, ref text); + var sendGreetEnabled = _greetService.SetGreetMessage(Context.Guild.Id, ref text); await ReplyConfirmLocalized("greetmsg_new").ConfigureAwait(false); if (!sendGreetEnabled) @@ -246,7 +79,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task GreetDm() { - var enabled = await greetService.SetGreetDm(Context.Guild.Id).ConfigureAwait(false); + var enabled = await _greetService.SetGreetDm(Context.Guild.Id).ConfigureAwait(false); if (enabled) await ReplyConfirmLocalized("greetdm_on").ConfigureAwait(false); @@ -262,7 +95,7 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(text)) { GuildConfig config; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { config = uow.GuildConfigs.For(Context.Guild.Id); } @@ -270,7 +103,7 @@ namespace NadekoBot.Modules.Administration return; } - var sendGreetEnabled = greetService.SetGreetDmMessage(Context.Guild.Id, ref text); + var sendGreetEnabled = _greetService.SetGreetDmMessage(Context.Guild.Id, ref text); await ReplyConfirmLocalized("greetdmmsg_new").ConfigureAwait(false); if (!sendGreetEnabled) @@ -282,7 +115,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task Bye() { - var enabled = await greetService.SetBye(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); + var enabled = await _greetService.SetBye(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); if (enabled) await ReplyConfirmLocalized("bye_on").ConfigureAwait(false); @@ -298,7 +131,7 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(text)) { string byeMessageText; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { byeMessageText = uow.GuildConfigs.For(Context.Guild.Id, set => set).ChannelByeMessageText; } @@ -306,7 +139,7 @@ namespace NadekoBot.Modules.Administration return; } - var sendByeEnabled = greetService.SetByeMessage(Context.Guild.Id, ref text); + var sendByeEnabled = _greetService.SetByeMessage(Context.Guild.Id, ref text); await ReplyConfirmLocalized("byemsg_new").ConfigureAwait(false); if (!sendByeEnabled) @@ -318,7 +151,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task ByeDel(int timer = 30) { - await greetService.SetByeDel(Context.Guild.Id, timer).ConfigureAwait(false); + await _greetService.SetByeDel(Context.Guild.Id, timer).ConfigureAwait(false); if (timer > 0) await ReplyConfirmLocalized("byedel_on", timer).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index f0189683..5a2dfde6 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; @@ -18,6 +19,16 @@ namespace NadekoBot.Modules.Administration [Group] public class UserPunishCommands : NadekoSubmodule { + private readonly DbHandler _db; + private readonly MuteService _muteService; + + public UserPunishCommands(DbHandler db, MuteService muteService) + { + _db = db; + + _muteService = muteService; + } + private async Task InternalWarn(IGuild guild, ulong userId, string modName, string reason) { if (string.IsNullOrWhiteSpace(reason)) @@ -36,7 +47,7 @@ namespace NadekoBot.Modules.Administration int warnings = 1; List ps; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { ps = uow.GuildConfigs.For(guildId, set => set.Include(x => x.WarnPunishments)) .WarnPunishments; @@ -62,9 +73,9 @@ namespace NadekoBot.Modules.Administration { case PunishmentAction.Mute: if (p.Time == 0) - await MuteCommands.MuteUser(user).ConfigureAwait(false); + await _muteService.MuteUser(user).ConfigureAwait(false); else - await MuteCommands.TimedMute(user, TimeSpan.FromMinutes(p.Time)).ConfigureAwait(false); + await _muteService.TimedMute(user, TimeSpan.FromMinutes(p.Time)).ConfigureAwait(false); break; case PunishmentAction.Kick: await user.KickAsync().ConfigureAwait(false); @@ -147,7 +158,7 @@ namespace NadekoBot.Modules.Administration if (page < 0) return; Warning[] warnings; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { warnings = uow.Warnings.For(Context.Guild.Id, userId); } @@ -192,7 +203,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.BanMembers)] public async Task Warnclear(ulong userId) { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { await uow.Warnings.ForgiveAll(Context.Guild.Id, userId, Context.User.ToString()).ConfigureAwait(false); uow.Complete(); @@ -212,7 +223,7 @@ namespace NadekoBot.Modules.Administration if (number <= 0) return; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var ps = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.WarnPunishments)).WarnPunishments; ps.RemoveAll(x => x.Count == number); @@ -239,7 +250,7 @@ namespace NadekoBot.Modules.Administration if (number <= 0) return; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var ps = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.WarnPunishments)).WarnPunishments; var p = ps.FirstOrDefault(x => x.Count == number); @@ -260,7 +271,7 @@ namespace NadekoBot.Modules.Administration public async Task WarnPunishList() { WarningPunishment[] ps; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { ps = uow.GuildConfigs.For(Context.Guild.Id, gc => gc.Include(x => x.WarnPunishments)) .WarnPunishments diff --git a/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs b/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs index e95fc22e..177dcd60 100644 --- a/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { @@ -18,86 +19,13 @@ namespace NadekoBot.Modules.Administration [Group] public class VcRoleCommands : NadekoSubmodule { - private static ConcurrentDictionary> VcRoles { get; } + private readonly VcRoleService _service; + private readonly DbHandler _db; - static VcRoleCommands() + public VcRoleCommands(VcRoleService service, DbHandler db) { - NadekoBot.Client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated; - VcRoles = new ConcurrentDictionary>(); - foreach (var gconf in NadekoBot.AllGuildConfigs) - { - var g = NadekoBot.Client.GetGuild(gconf.GuildId); - if (g == null) - continue; //todo delete everything from db if guild doesn't exist? - - var infos = new ConcurrentDictionary(); - VcRoles.TryAdd(gconf.GuildId, infos); - foreach (var ri in gconf.VcRoleInfos) - { - var role = g.GetRole(ri.RoleId); - if (role == null) - continue; //todo remove this entry from db - - infos.TryAdd(ri.VoiceChannelId, role); - } - } - } - - private static Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, - SocketVoiceState newState) - { - - var gusr = usr as SocketGuildUser; - if (gusr == null) - return Task.CompletedTask; - - var oldVc = oldState.VoiceChannel; - var newVc = newState.VoiceChannel; - var _ = Task.Run(async () => - { - try - { - if (oldVc != newVc) - { - ulong guildId; - guildId = newVc?.Guild.Id ?? oldVc.Guild.Id; - - if (VcRoles.TryGetValue(guildId, out ConcurrentDictionary guildVcRoles)) - { - //remove old - if (oldVc != null && guildVcRoles.TryGetValue(oldVc.Id, out IRole role)) - { - if (gusr.Roles.Contains(role)) - { - try - { - await gusr.RemoveRoleAsync(role).ConfigureAwait(false); - await Task.Delay(500).ConfigureAwait(false); - } - catch - { - await Task.Delay(200).ConfigureAwait(false); - await gusr.RemoveRoleAsync(role).ConfigureAwait(false); - await Task.Delay(500).ConfigureAwait(false); - } - } - } - //add new - if (newVc != null && guildVcRoles.TryGetValue(newVc.Id, out role)) - { - if (!gusr.Roles.Contains(role)) - await gusr.AddRoleAsync(role).ConfigureAwait(false); - } - - } - } - } - catch (Exception ex) - { - Administration._log.Warn(ex); - } - }); - return Task.CompletedTask; + _service = service; + _db = db; } [NadekoCommand, Usage, Description, Aliases] @@ -118,14 +46,14 @@ namespace NadekoBot.Modules.Administration return; } - var guildVcRoles = VcRoles.GetOrAdd(user.GuildId, new ConcurrentDictionary()); + var guildVcRoles = _service.VcRoles.GetOrAdd(user.GuildId, new ConcurrentDictionary()); if (role == null) { if (guildVcRoles.TryRemove(vc.Id, out role)) { await ReplyConfirmLocalized("vcrole_removed", Format.Bold(vc.Name)).ConfigureAwait(false); - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var conf = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.VcRoleInfos)); conf.VcRoleInfos.RemoveWhere(x => x.VoiceChannelId == vc.Id); @@ -136,7 +64,7 @@ namespace NadekoBot.Modules.Administration else { guildVcRoles.AddOrUpdate(vc.Id, role, (key, old) => role); - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var conf = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.VcRoleInfos)); conf.VcRoleInfos.RemoveWhere(x => x.VoiceChannelId == vc.Id); // remove old one @@ -157,7 +85,7 @@ namespace NadekoBot.Modules.Administration { var guild = (SocketGuild) Context.Guild; string text; - if (VcRoles.TryGetValue(Context.Guild.Id, out ConcurrentDictionary roles)) + if (_service.VcRoles.TryGetValue(Context.Guild.Id, out ConcurrentDictionary roles)) { if (!roles.Any()) { diff --git a/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs b/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs index 22820fe8..0a5a8835 100644 --- a/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs @@ -4,6 +4,7 @@ using Discord.WebSocket; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NLog; using System; using System.Collections.Concurrent; @@ -21,140 +22,15 @@ namespace NadekoBot.Modules.Administration [Group] public class VoicePlusTextCommands : NadekoSubmodule { - private new static readonly Logger _log; + private readonly VplusTService _service; + private readonly DbHandler _db; - private static readonly Regex _channelNameRegex = new Regex(@"[^a-zA-Z0-9 -]", RegexOptions.Compiled); - - private static readonly ConcurrentHashSet _voicePlusTextCache; - private static readonly ConcurrentDictionary _guildLockObjects = new ConcurrentDictionary(); - static VoicePlusTextCommands() + public VoicePlusTextCommands(VplusTService service, DbHandler db) { - _log = LogManager.GetCurrentClassLogger(); - var sw = Stopwatch.StartNew(); - - _voicePlusTextCache = new ConcurrentHashSet(NadekoBot.AllGuildConfigs.Where(g => g.VoicePlusTextEnabled).Select(g => g.GuildId)); - NadekoBot.Client.UserVoiceStateUpdated += UserUpdatedEventHandler; - - sw.Stop(); - _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); + _service = service; + _db = db; } - private static Task UserUpdatedEventHandler(SocketUser iuser, SocketVoiceState before, SocketVoiceState after) - { - var user = (iuser as SocketGuildUser); - var guild = user?.Guild; - - if (guild == null) - return Task.CompletedTask; - - var botUserPerms = guild.CurrentUser.GuildPermissions; - - if (before.VoiceChannel == after.VoiceChannel) - return Task.CompletedTask; - - if (!_voicePlusTextCache.Contains(guild.Id)) - return Task.CompletedTask; - - var _ = Task.Run(async () => - { - try - { - - if (!botUserPerms.ManageChannels || !botUserPerms.ManageRoles) - { - try - { - await guild.Owner.SendErrorAsync( - GetTextStatic("vt_exit", - NadekoBot.Localization.GetCultureInfo(guild), - typeof(Administration).Name.ToLowerInvariant(), - Format.Bold(guild.Name))).ConfigureAwait(false); - } - catch - { - // ignored - } - using (var uow = DbHandler.UnitOfWork()) - { - uow.GuildConfigs.For(guild.Id, set => set).VoicePlusTextEnabled = false; - _voicePlusTextCache.TryRemove(guild.Id); - await uow.CompleteAsync().ConfigureAwait(false); - } - return; - } - - var semaphore = _guildLockObjects.GetOrAdd(guild.Id, (key) => new SemaphoreSlim(1, 1)); - - try - { - await semaphore.WaitAsync().ConfigureAwait(false); - - var beforeVch = before.VoiceChannel; - if (beforeVch != null) - { - var beforeRoleName = GetRoleName(beforeVch); - var beforeRole = guild.Roles.FirstOrDefault(x => x.Name == beforeRoleName); - if (beforeRole != null) - try - { - _log.Info("Removing role " + beforeRoleName + " from user " + user.Username); - await user.RemoveRoleAsync(beforeRole).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn(ex); - } - } - var afterVch = after.VoiceChannel; - if (afterVch != null && guild.AFKChannel?.Id != afterVch.Id) - { - var roleName = GetRoleName(afterVch); - var roleToAdd = guild.Roles.FirstOrDefault(x => x.Name == roleName) ?? - (IRole) await guild.CreateRoleAsync(roleName, GuildPermissions.None).ConfigureAwait(false); - - ITextChannel textChannel = guild.TextChannels - .FirstOrDefault(t => t.Name == GetChannelName(afterVch.Name).ToLowerInvariant()); - if (textChannel == null) - { - var created = (await guild.CreateTextChannelAsync(GetChannelName(afterVch.Name).ToLowerInvariant()).ConfigureAwait(false)); - - try { await guild.CurrentUser.AddRoleAsync(roleToAdd).ConfigureAwait(false); } catch {/*ignored*/} - await Task.Delay(50).ConfigureAwait(false); - await created.AddPermissionOverwriteAsync(roleToAdd, new OverwritePermissions( - readMessages: PermValue.Allow, - sendMessages: PermValue.Allow)) - .ConfigureAwait(false); - await Task.Delay(50).ConfigureAwait(false); - await created.AddPermissionOverwriteAsync(guild.EveryoneRole, new OverwritePermissions( - readMessages: PermValue.Deny, - sendMessages: PermValue.Deny)) - .ConfigureAwait(false); - await Task.Delay(50).ConfigureAwait(false); - } - _log.Warn("Adding role " + roleToAdd.Name + " to user " + user.Username); - await user.AddRoleAsync(roleToAdd).ConfigureAwait(false); - } - } - finally - { - semaphore.Release(); - } - } - catch (Exception ex) - { - _log.Warn(ex); - } - }); - return Task.CompletedTask; - } - - private static string GetChannelName(string voiceName) => - _channelNameRegex.Replace(voiceName, "").Trim().Replace(" ", "-").TrimTo(90, true) + "-voice"; - - private static string GetRoleName(IVoiceChannel ch) => - "nvoice-" + ch.Id; - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageRoles)] @@ -184,7 +60,7 @@ namespace NadekoBot.Modules.Administration try { bool isEnabled; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var conf = uow.GuildConfigs.For(guild.Id, set => set); isEnabled = conf.VoicePlusTextEnabled = !conf.VoicePlusTextEnabled; @@ -192,7 +68,7 @@ namespace NadekoBot.Modules.Administration } if (!isEnabled) { - _voicePlusTextCache.TryRemove(guild.Id); + _service.VoicePlusTextCache.TryRemove(guild.Id); foreach (var textChannel in (await guild.GetTextChannelsAsync().ConfigureAwait(false)).Where(c => c.Name.EndsWith("-voice"))) { try { await textChannel.DeleteAsync().ConfigureAwait(false); } catch { } @@ -207,7 +83,7 @@ namespace NadekoBot.Modules.Administration await ReplyConfirmLocalized("vt_disabled").ConfigureAwait(false); return; } - _voicePlusTextCache.Add(guild.Id); + _service.VoicePlusTextCache.Add(guild.Id); await ReplyConfirmLocalized("vt_enabled").ConfigureAwait(false); } @@ -236,7 +112,7 @@ namespace NadekoBot.Modules.Administration var voiceChannels = await guild.GetVoiceChannelsAsync().ConfigureAwait(false); var boundTextChannels = textChannels.Where(c => c.Name.EndsWith("-voice")); - var validTxtChannelNames = new HashSet(voiceChannels.Select(c => GetChannelName(c.Name).ToLowerInvariant())); + var validTxtChannelNames = new HashSet(voiceChannels.Select(c => _service.GetChannelName(c.Name).ToLowerInvariant())); var invalidTxtChannels = boundTextChannels.Where(c => !validTxtChannelNames.Contains(c.Name)); foreach (var c in invalidTxtChannels) @@ -246,7 +122,7 @@ namespace NadekoBot.Modules.Administration } var boundRoles = guild.Roles.Where(r => r.Name.StartsWith("nvoice-")); - var validRoleNames = new HashSet(voiceChannels.Select(c => GetRoleName(c).ToLowerInvariant())); + var validRoleNames = new HashSet(voiceChannels.Select(c => _service.GetRoleName(c).ToLowerInvariant())); var invalidRoles = boundRoles.Where(r => !validRoleNames.Contains(r.Name)); foreach (var r in invalidRoles) diff --git a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs index f90f946b..2b3494be 100644 --- a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs +++ b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs @@ -21,9 +21,16 @@ namespace NadekoBot.Modules.Games [Group] public class Acropobia : NadekoSubmodule { + private readonly DiscordShardedClient _client; + //channelId, game public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); + public Acropobia(DiscordShardedClient client) + { + _client = client; + } + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Acro(int time = 60) @@ -32,7 +39,7 @@ namespace NadekoBot.Modules.Games return; var channel = (ITextChannel)Context.Channel; - var game = new AcrophobiaGame(channel, time); + var game = new AcrophobiaGame(_client, _strings, channel, time); if (AcrophobiaGames.TryAdd(channel.Id, game)) { try @@ -59,6 +66,7 @@ namespace NadekoBot.Modules.Games Voting } + //todo Isolate, this shouldn't print or anything like that. public class AcrophobiaGame { private readonly ITextChannel _channel; @@ -79,10 +87,14 @@ namespace NadekoBot.Modules.Games //text, votes private readonly ConcurrentDictionary _votes = new ConcurrentDictionary(); private readonly Logger _log; + private readonly DiscordShardedClient _client; + private readonly NadekoStrings _strings; - public AcrophobiaGame(ITextChannel channel, int time) + public AcrophobiaGame(DiscordShardedClient client, NadekoStrings strings, ITextChannel channel, int time) { _log = LogManager.GetCurrentClassLogger(); + _client = client; + _strings = strings; _channel = channel; _time = time; @@ -123,7 +135,7 @@ $@"-- public async Task Run() { - NadekoBot.Client.MessageReceived += PotentialAcro; + _client.MessageReceived += PotentialAcro; var embed = GetEmbed(); //SUBMISSIONS PHASE @@ -292,14 +304,14 @@ $@"-- public void EnsureStopped() { - NadekoBot.Client.MessageReceived -= PotentialAcro; + _client.MessageReceived -= PotentialAcro; if (!_source.IsCancellationRequested) _source.Cancel(); } private string GetText(string key, params object[] replacements) - => GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(_channel.Guild), + => _strings.GetText(key, + _channel.Guild.Id, typeof(Games).Name.ToLowerInvariant(), replacements); } diff --git a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs index 8abb47d5..f9f6fcbd 100644 --- a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs @@ -1,16 +1,10 @@ using Discord; using Discord.Commands; using NadekoBot.Attributes; -using NadekoBot.Extensions; using NadekoBot.Services; -using NLog; using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; using System.Threading.Tasks; -using Newtonsoft.Json; +using NadekoBot.Services.Games; namespace NadekoBot.Modules.Games { @@ -19,72 +13,13 @@ namespace NadekoBot.Modules.Games [Group] public class CleverBotCommands : NadekoSubmodule { - private new static Logger _log { get; } + private readonly DbHandler _db; + private readonly GamesService _games; - public static ConcurrentDictionary> CleverbotGuilds { get; } - - static CleverBotCommands() + public CleverBotCommands(DbHandler db, GamesService games) { - _log = LogManager.GetCurrentClassLogger(); - var sw = Stopwatch.StartNew(); - - CleverbotGuilds = new ConcurrentDictionary>( - NadekoBot.AllGuildConfigs - .Where(gc => gc.CleverbotEnabled) - .ToDictionary(gc => gc.GuildId, gc => new Lazy(() => new ChatterBotSession(gc.GuildId), true))); - - sw.Stop(); - _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); - } - - public static string PrepareMessage(IUserMessage msg, out ChatterBotSession cleverbot) - { - var channel = msg.Channel as ITextChannel; - cleverbot = null; - - if (channel == null) - return null; - - Lazy lazyCleverbot; - if (!CleverbotGuilds.TryGetValue(channel.Guild.Id, out lazyCleverbot)) - return null; - - cleverbot = lazyCleverbot.Value; - - var nadekoId = NadekoBot.Client.CurrentUser.Id; - var normalMention = $"<@{nadekoId}> "; - var nickMention = $"<@!{nadekoId}> "; - string message; - if (msg.Content.StartsWith(normalMention)) - { - message = msg.Content.Substring(normalMention.Length).Trim(); - } - else if (msg.Content.StartsWith(nickMention)) - { - message = msg.Content.Substring(nickMention.Length).Trim(); - } - else - { - return null; - } - - return message; - } - - public static async Task TryAsk(ChatterBotSession cleverbot, ITextChannel channel, string message) - { - await channel.TriggerTypingAsync().ConfigureAwait(false); - - var response = await cleverbot.Think(message).ConfigureAwait(false); - try - { - await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false); - } - catch - { - await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false); // try twice :\ - } - return true; + _db = db; + _games = games; } [NadekoCommand, Usage, Description, Aliases] @@ -94,10 +29,9 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - Lazy throwaway; - if (CleverbotGuilds.TryRemove(channel.Guild.Id, out throwaway)) + if (_games.CleverbotGuilds.TryRemove(channel.Guild.Id, out Lazy throwaway)) { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, false); await uow.CompleteAsync().ConfigureAwait(false); @@ -106,9 +40,9 @@ namespace NadekoBot.Modules.Games return; } - CleverbotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => new ChatterBotSession(Context.Guild.Id), true)); + _games.CleverbotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => new ChatterBotSession(Context.Guild.Id), true)); - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, true); await uow.CompleteAsync().ConfigureAwait(false); @@ -118,41 +52,6 @@ namespace NadekoBot.Modules.Games } } - public class ChatterBotSession - { - private static NadekoRandom rng { get; } = new NadekoRandom(); - public string ChatterbotId { get; } - public string ChannelId { get; } - private int _botId = 6; - - public ChatterBotSession(ulong channelId) - { - ChannelId = channelId.ToString().ToBase64(); - ChatterbotId = rng.Next(0, 1000000).ToString().ToBase64(); - } - - private string apiEndpoint => "http://api.program-o.com/v2/chatbot/" + - $"?bot_id={_botId}&" + - "say={0}&" + - $"convo_id=nadekobot_{ChatterbotId}_{ChannelId}&" + - "format=json"; - - public async Task Think(string message) - { - using (var http = new HttpClient()) - { - var res = await http.GetStringAsync(string.Format(apiEndpoint, message)).ConfigureAwait(false); - var cbr = JsonConvert.DeserializeObject(res); - //Console.WriteLine(cbr.Convo_id); - return cbr.BotSay.Replace("
", "\n"); - } - } - } - - public class ChatterBotResponse - { - public string Convo_id { get; set; } - public string BotSay { get; set; } - } + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs index 5c329925..c59ada3a 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs @@ -56,6 +56,7 @@ namespace NadekoBot.Modules.Games.Hangman public class HangmanGame: IDisposable { private readonly Logger _log; + private readonly DiscordShardedClient _client; public IMessageChannel GameChannel { get; } public HashSet Guesses { get; } = new HashSet(); @@ -81,9 +82,11 @@ namespace NadekoBot.Modules.Games.Hangman public event Action OnEnded; - public HangmanGame(IMessageChannel channel, string type) + public HangmanGame(DiscordShardedClient client, IMessageChannel channel, string type) { _log = LogManager.GetCurrentClassLogger(); + _client = client; + this.GameChannel = channel; this.TermType = type.ToTitleCase(); } @@ -95,12 +98,12 @@ namespace NadekoBot.Modules.Games.Hangman if (this.Term == null) throw new KeyNotFoundException("Can't find a term with that type. Use hangmanlist command."); // start listening for answers when game starts - NadekoBot.Client.MessageReceived += PotentialGuess; + _client.MessageReceived += PotentialGuess; } public async Task End() { - NadekoBot.Client.MessageReceived -= PotentialGuess; + _client.MessageReceived -= PotentialGuess; OnEnded(this); var toSend = "Game ended. You **" + (Errors >= MaxErrors ? "LOSE" : "WIN") + "**!\n" + GetHangman(); var embed = new EmbedBuilder().WithTitle("Hangman Game") @@ -199,7 +202,7 @@ namespace NadekoBot.Modules.Games.Hangman public void Dispose() { - NadekoBot.Client.MessageReceived -= PotentialGuess; + _client.MessageReceived -= PotentialGuess; OnEnded = null; } } diff --git a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs index 4038583d..633be81f 100644 --- a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using NadekoBot.Modules.Games.Hangman; using Discord; +using Discord.WebSocket; namespace NadekoBot.Modules.Games { @@ -14,6 +15,13 @@ namespace NadekoBot.Modules.Games [Group] public class HangmanCommands : NadekoSubmodule { + private readonly DiscordShardedClient _client; + + public HangmanCommands(DiscordShardedClient client) + { + _client = client; + } + //channelId, game public static ConcurrentDictionary HangmanGames { get; } = new ConcurrentDictionary(); [NadekoCommand, Usage, Description, Aliases] @@ -25,7 +33,7 @@ namespace NadekoBot.Modules.Games [NadekoCommand, Usage, Description, Aliases] public async Task Hangman([Remainder]string type = "All") { - var hm = new HangmanGame(Context.Channel, type); + var hm = new HangmanGame(_client, Context.Channel, type); if (!HangmanGames.TryAdd(Context.Channel.Id, hm)) { @@ -35,8 +43,7 @@ namespace NadekoBot.Modules.Games hm.OnEnded += g => { - HangmanGame throwaway; - HangmanGames.TryRemove(g.GameChannel.Id, out throwaway); + HangmanGames.TryRemove(g.GameChannel.Id, out HangmanGame throwaway); }; try { @@ -45,8 +52,7 @@ namespace NadekoBot.Modules.Games catch (Exception ex) { try { await Context.Channel.SendErrorAsync(GetText("hangman_start_errored") + " " + ex.Message).ConfigureAwait(false); } catch { } - HangmanGame throwaway; - HangmanGames.TryRemove(Context.Channel.Id, out throwaway); + HangmanGames.TryRemove(Context.Channel.Id, out HangmanGame throwaway); throwaway.Dispose(); return; } diff --git a/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs b/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs new file mode 100644 index 00000000..a91559ac --- /dev/null +++ b/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs @@ -0,0 +1,147 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Games; +using NLog; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Models +{ + public class TypingGame + { + public const float WORD_VALUE = 4.5f; + public ITextChannel Channel { get; } + public string CurrentSentence { get; private set; } + public bool IsActive { get; private set; } + private readonly Stopwatch sw; + private readonly List finishedUserIds; + private readonly DiscordShardedClient _client; + private readonly GamesService _games; + + private Logger _log { get; } + + public TypingGame(GamesService games, DiscordShardedClient client, ITextChannel channel) + { + _log = LogManager.GetCurrentClassLogger(); + _games = games; + _client = client; + + this.Channel = channel; + IsActive = false; + sw = new Stopwatch(); + finishedUserIds = new List(); + } + + public async Task Stop() + { + if (!IsActive) return false; + _client.MessageReceived -= AnswerReceived; + finishedUserIds.Clear(); + IsActive = false; + sw.Stop(); + sw.Reset(); + try { await Channel.SendConfirmAsync("Typing contest stopped.").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + return true; + } + + public async Task Start() + { + if (IsActive) return; // can't start running game + IsActive = true; + CurrentSentence = GetRandomSentence(); + var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f); + try + { + await Channel.SendConfirmAsync($@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.").ConfigureAwait(false); + + + var msg = await Channel.SendMessageAsync("Starting new typing contest in **3**...").ConfigureAwait(false); + await Task.Delay(1000).ConfigureAwait(false); + try + { + await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **2**...").ConfigureAwait(false); + await Task.Delay(1000).ConfigureAwait(false); + await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **1**...").ConfigureAwait(false); + await Task.Delay(1000).ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex); } + + await msg.ModifyAsync(m => m.Content = Format.Bold(Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions())).ConfigureAwait(false); + sw.Start(); + HandleAnswers(); + + while (i > 0) + { + await Task.Delay(1000).ConfigureAwait(false); + i--; + if (!IsActive) + return; + } + + } + catch { } + finally + { + await Stop().ConfigureAwait(false); + } + } + + public string GetRandomSentence() + { + if (_games.TypingArticles.Any()) + return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text; + else + return $"No typing articles found. Use {NadekoBot.Prefix}typeadd command to add a new article for typing."; + + } + + private void HandleAnswers() + { + _client.MessageReceived += AnswerReceived; + } + + private async Task AnswerReceived(SocketMessage imsg) + { + try + { + if (imsg.Author.IsBot) + return; + var msg = imsg as SocketUserMessage; + if (msg == null) + return; + + if (this.Channel == null || this.Channel.Id != msg.Channel.Id) return; + + var guess = msg.Content; + + var distance = CurrentSentence.LevenshteinDistance(guess); + var decision = Judge(distance, guess.Length); + if (decision && !finishedUserIds.Contains(msg.Author.Id)) + { + var elapsed = sw.Elapsed; + var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60; + finishedUserIds.Add(msg.Author.Id); + await this.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithTitle($"{msg.Author} finished the race!") + .AddField(efb => efb.WithName("Place").WithValue($"#{finishedUserIds.Count}").WithIsInline(true)) + .AddField(efb => efb.WithName("WPM").WithValue($"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*").WithIsInline(true)) + .AddField(efb => efb.WithName("Errors").WithValue(distance.ToString()).WithIsInline(true))) + .ConfigureAwait(false); + if (finishedUserIds.Count % 4 == 0) + { + await this.Channel.SendConfirmAsync($":exclamation: A lot of people finished, here is the text for those still typing:\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions()}**").ConfigureAwait(false); + } + } + } + catch (Exception ex) { _log.Warn(ex); } + } + + private bool Judge(int errors, int textLength) => errors <= textLength / 25; + + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs index 26eda48a..bf837979 100644 --- a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs @@ -6,6 +6,7 @@ using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Games; using NLog; using System; using System.Collections.Concurrent; @@ -28,89 +29,20 @@ namespace NadekoBot.Modules.Games [Group] public class PlantPickCommands : NadekoSubmodule { - private static ConcurrentHashSet generationChannels { get; } - //channelid/message - private static ConcurrentDictionary> plantedFlowers { get; } = new ConcurrentDictionary>(); - //channelId/last generation - private static ConcurrentDictionary lastGenerations { get; } = new ConcurrentDictionary(); + private readonly CurrencyHandler _ch; + private readonly BotConfig _bc; + private readonly GamesService _games; + private readonly DbHandler _db; - static PlantPickCommands() + public PlantPickCommands(BotConfig bc, CurrencyHandler ch, GamesService games, + DbHandler db) { - NadekoBot.Client.MessageReceived += PotentialFlowerGeneration; - generationChannels = new ConcurrentHashSet(NadekoBot.AllGuildConfigs - .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); + _bc = bc; + _ch = ch; + _games = games; + _db = db; } - private static Task PotentialFlowerGeneration(SocketMessage imsg) - { - var msg = imsg as SocketUserMessage; - if (msg == null || msg.IsAuthor() || msg.Author.IsBot) - return Task.CompletedTask; - - var channel = imsg.Channel as ITextChannel; - if (channel == null) - return Task.CompletedTask; - - if (!generationChannels.Contains(channel.Id)) - return Task.CompletedTask; - - var _ = Task.Run(async () => - { - try - { - var lastGeneration = lastGenerations.GetOrAdd(channel.Id, DateTime.MinValue); - var rng = new NadekoRandom(); - - //todo i'm stupid :rofl: wtg kwoth. real async programming :100: :ok_hand: :100: :100: :thumbsup: - if (DateTime.Now - TimeSpan.FromSeconds(NadekoBot.BotConfig.CurrencyGenerationCooldown) < lastGeneration) //recently generated in this channel, don't generate again - return; - - var num = rng.Next(1, 101) + NadekoBot.BotConfig.CurrencyGenerationChance * 100; - - if (num > 100) - { - lastGenerations.AddOrUpdate(channel.Id, DateTime.Now, (id, old) => DateTime.Now); - - var dropAmount = NadekoBot.BotConfig.CurrencyDropAmount; - - if (dropAmount > 0) - { - var msgs = new IUserMessage[dropAmount]; - var prefix = NadekoBot.ModulePrefixes[typeof(Games).Name]; - var toSend = dropAmount == 1 - ? GetLocalText(channel, "curgen_sn", NadekoBot.BotConfig.CurrencySign) - + " " + GetLocalText(channel, "pick_sn", prefix) - : GetLocalText(channel, "curgen_pl", dropAmount, NadekoBot.BotConfig.CurrencySign) - + " " + GetLocalText(channel, "pick_pl", prefix); - var file = GetRandomCurrencyImage(); - using (var fileStream = file.Value.ToStream()) - { - var sent = await channel.SendFileAsync( - fileStream, - file.Key, - toSend).ConfigureAwait(false); - - msgs[0] = sent; - } - - plantedFlowers.AddOrUpdate(channel.Id, msgs.ToList(), (id, old) => { old.AddRange(msgs); return old; }); - } - } - } - catch (Exception ex) - { - LogManager.GetCurrentClassLogger().Warn(ex); - } - }); - return Task.CompletedTask; - } - - public static string GetLocalText(ITextChannel channel, string key, params object[] replacements) => - NadekoTopLevelModule.GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(channel.GuildId), - typeof(Games).Name.ToLowerInvariant(), - replacements); - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Pick() @@ -120,16 +52,15 @@ namespace NadekoBot.Modules.Games if (!(await channel.Guild.GetCurrentUserAsync()).GetPermissions(channel).ManageMessages) return; - List msgs; try { await Context.Message.DeleteAsync().ConfigureAwait(false); } catch { } - if (!plantedFlowers.TryRemove(channel.Id, out msgs)) + if (!_games.PlantedFlowers.TryRemove(channel.Id, out List msgs)) return; await Task.WhenAll(msgs.Where(m => m != null).Select(toDelete => toDelete.DeleteAsync())).ConfigureAwait(false); - await CurrencyHandler.AddCurrencyAsync((IGuildUser)Context.User, $"Picked {NadekoBot.BotConfig.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false); - var msg = await ReplyConfirmLocalized("picked", msgs.Count + NadekoBot.BotConfig.CurrencySign) + await _ch.AddCurrencyAsync((IGuildUser)Context.User, $"Picked {_bc.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false); + var msg = await ReplyConfirmLocalized("picked", msgs.Count + _bc.CurrencySign) .ConfigureAwait(false); msg.DeleteAfter(10); } @@ -141,21 +72,21 @@ namespace NadekoBot.Modules.Games if (amount < 1) return; - var removed = await CurrencyHandler.RemoveCurrencyAsync((IGuildUser)Context.User, $"Planted a {NadekoBot.BotConfig.CurrencyName}", amount, false).ConfigureAwait(false); + var removed = await _ch.RemoveCurrencyAsync((IGuildUser)Context.User, $"Planted a {_bc.CurrencyName}", amount, false).ConfigureAwait(false); if (!removed) { - await ReplyErrorLocalized("not_enough", NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.CurrencySign).ConfigureAwait(false); return; } - var imgData = GetRandomCurrencyImage(); + var imgData = _games.GetRandomCurrencyImage(); //todo upload all currency images to transfer.sh and use that one as cdn //and then var msgToSend = GetText("planted", Format.Bold(Context.User.ToString()), - amount + NadekoBot.BotConfig.CurrencySign, + amount + _bc.CurrencySign, Prefix); if (amount > 1) @@ -172,7 +103,7 @@ namespace NadekoBot.Modules.Games var msgs = new IUserMessage[amount]; msgs[0] = msg; - plantedFlowers.AddOrUpdate(Context.Channel.Id, msgs.ToList(), (id, old) => + _games.PlantedFlowers.AddOrUpdate(Context.Channel.Id, msgs.ToList(), (id, old) => { old.AddRange(msgs); return old; @@ -190,7 +121,7 @@ namespace NadekoBot.Modules.Games var channel = (ITextChannel)Context.Channel; bool enabled; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var guildConfig = uow.GuildConfigs.For(channel.Id, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); @@ -198,13 +129,13 @@ namespace NadekoBot.Modules.Games if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd)) { guildConfig.GenerateCurrencyChannelIds.Add(toAdd); - generationChannels.Add(channel.Id); + _games.GenerationChannels.Add(channel.Id); enabled = true; } else { guildConfig.GenerateCurrencyChannelIds.Remove(toAdd); - generationChannels.TryRemove(channel.Id); + _games.GenerationChannels.TryRemove(channel.Id); enabled = false; } await uow.CompleteAsync(); @@ -218,14 +149,6 @@ namespace NadekoBot.Modules.Games await ReplyConfirmLocalized("curgen_disabled").ConfigureAwait(false); } } - - private static KeyValuePair> GetRandomCurrencyImage() - { - var rng = new NadekoRandom(); - var images = NadekoBot.Images.Currency; - - return images[rng.Next(0, images.Length)]; - } } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index 5e18ff44..c5424bbe 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using ImageSharp.Processing; +using NadekoBot.Services; namespace NadekoBot.Modules.Games { @@ -19,6 +20,12 @@ namespace NadekoBot.Modules.Games public class PollCommands : NadekoSubmodule { public static ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); + private readonly DiscordShardedClient _client; + + public PollCommands(DiscordShardedClient client) + { + _client = client; + } [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(GuildPermission.ManageMessages)] @@ -54,7 +61,7 @@ namespace NadekoBot.Modules.Games if (data.Length < 3) return; - var poll = new Poll(Context.Message, data[0], data.Skip(1), isPublic: isPublic); + var poll = new Poll(_client, _strings, Context.Message, data[0], data.Skip(1), isPublic: isPublic); if (ActivePolls.TryAdd(channel.Guild.Id, poll)) { await poll.StartPoll().ConfigureAwait(false); @@ -83,10 +90,16 @@ namespace NadekoBot.Modules.Games private string[] answers { get; } private readonly ConcurrentDictionary _participants = new ConcurrentDictionary(); private readonly string _question; + private readonly DiscordShardedClient _client; + private readonly NadekoStrings _strings; + public bool IsPublic { get; } - public Poll(IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) + public Poll(DiscordShardedClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) { + _client = client; + _strings = strings; + _originalMessage = umsg; _guild = ((ITextChannel)umsg.Channel).Guild; _question = question; @@ -134,7 +147,7 @@ namespace NadekoBot.Modules.Games public async Task StartPoll() { - NadekoBot.Client.MessageReceived += Vote; + _client.MessageReceived += Vote; var msgToSend = GetText("poll_created", Format.Bold(_originalMessage.Author.Username)) + "\n\n" + Format.Bold(_question) + "\n"; var num = 1; msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n"); @@ -147,7 +160,7 @@ namespace NadekoBot.Modules.Games public async Task StopPoll() { - NadekoBot.Client.MessageReceived -= Vote; + _client.MessageReceived -= Vote; await _originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false); } @@ -205,8 +218,8 @@ namespace NadekoBot.Modules.Games } private string GetText(string key, params object[] replacements) - => NadekoTopLevelModule.GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(_guild.Id), + => _strings.GetText(key, + _guild.Id, typeof(Games).Name.ToLowerInvariant(), replacements); } diff --git a/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs b/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs index 048aab0f..25b7b12c 100644 --- a/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs @@ -3,14 +3,11 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Modules.Games.Commands.Models; -using NadekoBot.Services; +using NadekoBot.Modules.Games.Models; +using NadekoBot.Services.Games; using Newtonsoft.Json; -using NLog; using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -19,145 +16,18 @@ namespace NadekoBot.Modules.Games { public partial class Games { - public class TypingGame - { - public const float WORD_VALUE = 4.5f; - public ITextChannel Channel { get; } - public string CurrentSentence { get; private set; } - public bool IsActive { get; private set; } - private readonly Stopwatch sw; - private readonly List finishedUserIds; - private Logger _log { get; } - - public TypingGame(ITextChannel channel) - { - _log = LogManager.GetCurrentClassLogger(); - this.Channel = channel; - IsActive = false; - sw = new Stopwatch(); - finishedUserIds = new List(); - } - - public async Task Stop() - { - if (!IsActive) return false; - NadekoBot.Client.MessageReceived -= AnswerReceived; - finishedUserIds.Clear(); - IsActive = false; - sw.Stop(); - sw.Reset(); - try { await Channel.SendConfirmAsync("Typing contest stopped.").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - return true; - } - - public async Task Start() - { - if (IsActive) return; // can't start running game - IsActive = true; - CurrentSentence = GetRandomSentence(); - var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f); - try - { - await Channel.SendConfirmAsync($@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.").ConfigureAwait(false); - - - var msg = await Channel.SendMessageAsync("Starting new typing contest in **3**...").ConfigureAwait(false); - await Task.Delay(1000).ConfigureAwait(false); - try - { - await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **2**...").ConfigureAwait(false); - await Task.Delay(1000).ConfigureAwait(false); - await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **1**...").ConfigureAwait(false); - await Task.Delay(1000).ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex); } - - await msg.ModifyAsync(m => m.Content = Format.Bold(Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions())).ConfigureAwait(false); - sw.Start(); - HandleAnswers(); - - while (i > 0) - { - await Task.Delay(1000).ConfigureAwait(false); - i--; - if (!IsActive) - return; - } - - } - catch { } - finally - { - await Stop().ConfigureAwait(false); - } - } - - public string GetRandomSentence() - { - if (SpeedTypingCommands.TypingArticles.Any()) - return SpeedTypingCommands.TypingArticles[new NadekoRandom().Next(0, SpeedTypingCommands.TypingArticles.Count)].Text; - else - return $"No typing articles found. Use {NadekoBot.ModulePrefixes[typeof(Games).Name]}typeadd command to add a new article for typing."; - - } - - private void HandleAnswers() - { - NadekoBot.Client.MessageReceived += AnswerReceived; - } - - private async Task AnswerReceived(SocketMessage imsg) - { - try - { - if (imsg.Author.IsBot) - return; - var msg = imsg as SocketUserMessage; - if (msg == null) - return; - - if (this.Channel == null || this.Channel.Id != msg.Channel.Id) return; - - var guess = msg.Content; - - var distance = CurrentSentence.LevenshteinDistance(guess); - var decision = Judge(distance, guess.Length); - if (decision && !finishedUserIds.Contains(msg.Author.Id)) - { - var elapsed = sw.Elapsed; - var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60; - finishedUserIds.Add(msg.Author.Id); - await this.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithTitle($"{msg.Author} finished the race!") - .AddField(efb => efb.WithName("Place").WithValue($"#{finishedUserIds.Count}").WithIsInline(true)) - .AddField(efb => efb.WithName("WPM").WithValue($"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*").WithIsInline(true)) - .AddField(efb => efb.WithName("Errors").WithValue(distance.ToString()).WithIsInline(true))) - .ConfigureAwait(false); - if (finishedUserIds.Count % 4 == 0) - { - await this.Channel.SendConfirmAsync($":exclamation: A lot of people finished, here is the text for those still typing:\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions()}**").ConfigureAwait(false); - } - } - } - catch (Exception ex) { _log.Warn(ex); } - } - - private bool Judge(int errors, int textLength) => errors <= textLength / 25; - - } - [Group] public class SpeedTypingCommands : NadekoSubmodule { - public static List TypingArticles { get; } = new List(); - - private const string _typingArticlesPath = "data/typing_articles2.json"; - - static SpeedTypingCommands() - { - try { TypingArticles = JsonConvert.DeserializeObject>(File.ReadAllText(_typingArticlesPath)); } catch { } - } public static ConcurrentDictionary RunningContests = new ConcurrentDictionary(); + private readonly GamesService _games; + private readonly DiscordShardedClient _client; + + public SpeedTypingCommands(DiscordShardedClient client, GamesService games) + { + _games = games; + _client = client; + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -165,7 +35,7 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - var game = RunningContests.GetOrAdd(channel.Guild.Id, id => new TypingGame(channel)); + var game = RunningContests.GetOrAdd(channel.Guild.Id, id => new TypingGame(_games, _client, channel)); if (game.IsActive) { @@ -202,13 +72,14 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - TypingArticles.Add(new TypingArticle + _games.TypingArticles.Add(new TypingArticle { Title = $"Text added on {DateTime.UtcNow} by {Context.User}", Text = text.SanitizeMentions(), }); - File.WriteAllText(_typingArticlesPath, JsonConvert.SerializeObject(TypingArticles)); + //todo move this to service + File.WriteAllText(_games.TypingArticlesPath, JsonConvert.SerializeObject(_games.TypingArticles)); await channel.SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false); } @@ -222,7 +93,7 @@ namespace NadekoBot.Modules.Games if (page < 1) return; - var articles = TypingArticles.Skip((page - 1) * 15).Take(15).ToArray(); + var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray(); if (!articles.Any()) { @@ -242,13 +113,13 @@ namespace NadekoBot.Modules.Games var channel = (ITextChannel)Context.Channel; index -= 1; - if (index < 0 || index >= TypingArticles.Count) + if (index < 0 || index >= _games.TypingArticles.Count) return; - var removed = TypingArticles[index]; - TypingArticles.RemoveAt(index); + var removed = _games.TypingArticles[index]; + _games.TypingArticles.RemoveAt(index); - File.WriteAllText(_typingArticlesPath, JsonConvert.SerializeObject(TypingArticles)); + File.WriteAllText(_games.TypingArticlesPath, JsonConvert.SerializeObject(_games.TypingArticles)); await channel.SendConfirmAsync($"`Removed typing article:` #{index + 1} - {removed.Text.TrimTo(50)}") .ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs b/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs index 918a1592..c0e7e8b1 100644 --- a/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs +++ b/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs @@ -1,8 +1,9 @@ using Discord; using Discord.Commands; +using Discord.WebSocket; using NadekoBot.Attributes; using NadekoBot.Extensions; -using NLog; +using NadekoBot.Services; using System; using System.Collections.Generic; using System.Text; @@ -20,6 +21,12 @@ namespace NadekoBot.Modules.Games private static readonly Dictionary _games = new Dictionary(); private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1); + private readonly DiscordShardedClient _client; + + public TicTacToeCommands(DiscordShardedClient client) + { + _client = client; + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -39,7 +46,7 @@ namespace NadekoBot.Modules.Games }); return; } - game = new TicTacToe(channel, (IGuildUser)Context.User); + game = new TicTacToe(_strings, _client, channel, (IGuildUser)Context.User); _games.Add(channel.Id, game); await ReplyConfirmLocalized("ttt_created").ConfigureAwait(false); @@ -79,10 +86,15 @@ namespace NadekoBot.Modules.Games private IUserMessage _previousMessage; private Timer _timeoutTimer; + private readonly NadekoStrings _strings; + private readonly DiscordShardedClient _client; - public TicTacToe(ITextChannel channel, IGuildUser firstUser) + public TicTacToe(NadekoStrings strings, DiscordShardedClient client, ITextChannel channel, IGuildUser firstUser) { _channel = channel; + _strings = strings; + _client = client; + _users = new[] { firstUser, null }; _state = new int?[,] { { null, null, null }, @@ -95,8 +107,8 @@ namespace NadekoBot.Modules.Games } private string GetText(string key, params object[] replacements) => - NadekoTopLevelModule.GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(_channel.GuildId), + _strings.GetText(key, + _channel.GuildId, typeof(Games).Name.ToLowerInvariant(), replacements); @@ -206,7 +218,7 @@ namespace NadekoBot.Modules.Games } }, null, 15000, Timeout.Infinite); - NadekoBot.Client.MessageReceived += Client_MessageReceived; + _client.MessageReceived += Client_MessageReceived; _previousMessage = await _channel.EmbedAsync(GetEmbed(GetText("game_started"))).ConfigureAwait(false); @@ -283,14 +295,14 @@ namespace NadekoBot.Modules.Games { reason = GetText("ttt_matched_three"); _winner = _users[_curUserIndex]; - NadekoBot.Client.MessageReceived -= Client_MessageReceived; + _client.MessageReceived -= Client_MessageReceived; OnEnded?.Invoke(this); } else if (IsDraw()) { reason = GetText("ttt_a_draw"); _phase = Phase.Ended; - NadekoBot.Client.MessageReceived -= Client_MessageReceived; + _client.MessageReceived -= Client_MessageReceived; OnEnded?.Invoke(this); } diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs index 0fba8200..8b785dad 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs @@ -3,6 +3,7 @@ using Discord.Net; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Database.Models; using NLog; using System; using System.Collections.Concurrent; @@ -18,6 +19,10 @@ namespace NadekoBot.Modules.Games.Trivia { private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1); private readonly Logger _log; + private readonly NadekoStrings _strings; + private readonly DiscordShardedClient _client; + private readonly BotConfig _bc; + private readonly CurrencyHandler _ch; public IGuild Guild { get; } public ITextChannel Channel { get; } @@ -38,9 +43,15 @@ namespace NadekoBot.Modules.Games.Trivia public int WinRequirement { get; } - public TriviaGame(IGuild guild, ITextChannel channel, bool showHints, int winReq, bool isPokemon) + public TriviaGame(NadekoStrings strings, DiscordShardedClient client, BotConfig bc, + CurrencyHandler ch, IGuild guild, ITextChannel channel, + bool showHints, int winReq, bool isPokemon) { _log = LogManager.GetCurrentClassLogger(); + _strings = strings; + _client = client; + _bc = bc; + _ch = ch; ShowHints = showHints; Guild = guild; @@ -50,8 +61,8 @@ namespace NadekoBot.Modules.Games.Trivia } private string GetText(string key, params object[] replacements) => - NadekoTopLevelModule.GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(Channel.GuildId), + _strings.GetText(key, + Channel.GuildId, typeof(Games).Name.ToLowerInvariant(), replacements); @@ -99,7 +110,7 @@ namespace NadekoBot.Modules.Games.Trivia //receive messages try { - NadekoBot.Client.MessageReceived += PotentialGuess; + _client.MessageReceived += PotentialGuess; //allow people to guess GameActive = true; @@ -128,7 +139,7 @@ namespace NadekoBot.Modules.Games.Trivia finally { GameActive = false; - NadekoBot.Client.MessageReceived -= PotentialGuess; + _client.MessageReceived -= PotentialGuess; } if (!triviaCancelSource.IsCancellationRequested) { @@ -214,9 +225,9 @@ namespace NadekoBot.Modules.Games.Trivia { // ignored } - var reward = NadekoBot.BotConfig.TriviaCurrencyReward; + var reward = _bc.TriviaCurrencyReward; if (reward > 0) - await CurrencyHandler.AddCurrencyAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false); + await _ch.AddCurrencyAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false); return; } diff --git a/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs b/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs index a18763c1..f2b13e82 100644 --- a/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs @@ -1,8 +1,11 @@ using Discord; using Discord.Commands; +using Discord.WebSocket; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Modules.Games.Trivia; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; using System.Collections.Concurrent; using System.Threading.Tasks; @@ -14,8 +17,19 @@ namespace NadekoBot.Modules.Games [Group] public class TriviaCommands : NadekoSubmodule { + private readonly CurrencyHandler _ch; + private readonly DiscordShardedClient _client; + private readonly BotConfig _bc; + public static ConcurrentDictionary RunningTrivias { get; } = new ConcurrentDictionary(); + public TriviaCommands(DiscordShardedClient client, BotConfig bc, CurrencyHandler ch) + { + _ch = ch; + _client = client; + _bc = bc; + } + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public Task Trivia([Remainder] string additionalArgs = "") @@ -35,7 +49,7 @@ namespace NadekoBot.Modules.Games var showHints = !additionalArgs.Contains("nohint"); var isPokemon = additionalArgs.Contains("pokemon"); - var trivia = new TriviaGame(channel.Guild, channel, showHints, winReq, isPokemon); + var trivia = new TriviaGame(_strings, _client, _bc, _ch, channel.Guild, channel, showHints, winReq, isPokemon); if (RunningTrivias.TryAdd(channel.Guild.Id, trivia)) { try diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 5552dbf4..5257d710 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -14,19 +14,20 @@ using System.Net.Http; using ImageSharp; using NadekoBot.DataStructures; using NLog; +using NadekoBot.Services.Games; namespace NadekoBot.Modules.Games { - [NadekoModule("Games", ">")] public partial class Games : NadekoTopLevelModule { - private static readonly ImmutableArray _8BallResponses = NadekoBot.BotConfig.EightBallResponses.Select(ebr => ebr.Text).ToImmutableArray(); + private readonly GamesService _games; + private readonly IImagesService _images; - private static readonly Timer _t = new Timer((_) => + public Games(GamesService games, IImagesService images) { - _girlRatings.Clear(); - - }, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1)); + _games = games; + _images = images; + } [NadekoCommand, Usage, Description, Aliases] public async Task Choose([Remainder] string list = null) @@ -48,7 +49,7 @@ namespace NadekoBot.Modules.Games await Context.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) .AddField(efb => efb.WithName("❓ " + GetText("question") ).WithValue(question).WithIsInline(false)) - .AddField(efb => efb.WithName("🎱 " + GetText("8ball")).WithValue(_8BallResponses[new NadekoRandom().Next(0, _8BallResponses.Length)]).WithIsInline(false))); + .AddField(efb => efb.WithName("🎱 " + GetText("8ball")).WithValue(_games.EightBallResponses[new NadekoRandom().Next(0, _games.EightBallResponses.Length)]).WithIsInline(false))); } [NadekoCommand, Usage, Description, Aliases] @@ -94,7 +95,7 @@ namespace NadekoBot.Modules.Games else if ((pick == 0 && nadekoPick == 1) || (pick == 1 && nadekoPick == 2) || (pick == 2 && nadekoPick == 0)) - msg = GetText("rps_win", NadekoBot.Client.CurrentUser.Mention, + msg = GetText("rps_win", Context.Client.CurrentUser.Mention, getRpsPick(nadekoPick), getRpsPick(pick)); else msg = GetText("rps_win", Context.User.Mention, getRpsPick(pick), @@ -103,73 +104,12 @@ namespace NadekoBot.Modules.Games await Context.Channel.SendConfirmAsync(msg).ConfigureAwait(false); } - private static readonly ConcurrentDictionary _girlRatings = new ConcurrentDictionary(); - - public class GirlRating - { - private static readonly Logger _log = LogManager.GetCurrentClassLogger(); - - public double Crazy { get; } - public double Hot { get; } - public int Roll { get; } - public string Advice { get; } - public AsyncLazy Url { get; } - - public GirlRating(double crazy, double hot, int roll, string advice) - { - Crazy = crazy; - Hot = hot; - Roll = roll; - Advice = advice; // convenient to have it here, even though atm there are only few different ones. - - Url = new AsyncLazy(async () => - { - try - { - using (var ms = new MemoryStream(NadekoBot.Images.WifeMatrix.ToArray(), false)) - using (var img = new ImageSharp.Image(ms)) - { - const int minx = 35; - const int miny = 385; - const int length = 345; - - var pointx = (int)(minx + length * (Hot / 10)); - var pointy = (int)(miny - length * ((Crazy - 4) / 6)); - - using (var pointMs = new MemoryStream(NadekoBot.Images.RategirlDot.ToArray(), false)) - using (var pointImg = new ImageSharp.Image(pointMs)) - { - img.DrawImage(pointImg, 100, default(Size), new Point(pointx - 10, pointy - 10)); - } - - string url; - using (var http = new HttpClient()) - using (var imgStream = new MemoryStream()) - { - img.Save(imgStream); - var byteContent = new ByteArrayContent(imgStream.ToArray()); - http.AddFakeHeaders(); - - var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent); - url = await reponse.Content.ReadAsStringAsync(); - } - return url; - } - } - catch (Exception ex) - { - _log.Warn(ex); - return null; - } - }); - } - } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task RateGirl(IGuildUser usr) { - var gr = _girlRatings.GetOrAdd(usr.Id, GetGirl); + var gr = _games.GirlRatings.GetOrAdd(usr.Id, GetGirl); var img = await gr.Url; await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle("Girl Rating For " + usr) @@ -255,7 +195,7 @@ namespace NadekoBot.Modules.Games "and maybe look at how to replicate that."; } - return new GirlRating(crazy, hot, roll, advice); + return new GirlRating(_images, crazy, hot, roll, advice); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Searches/Commands/GoogleTranslator.cs b/src/NadekoBot/Modules/Searches/Commands/GoogleTranslator.cs deleted file mode 100644 index 9cd0645c..00000000 --- a/src/NadekoBot/Modules/Searches/Commands/GoogleTranslator.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -namespace NadekoBot.Modules.Searches -{ - public class GoogleTranslator - { - private static GoogleTranslator _instance; - public static GoogleTranslator Instance = _instance ?? (_instance = new GoogleTranslator()); - - public IEnumerable Languages => _languageDictionary.Keys.OrderBy(x => x); - private readonly Dictionary _languageDictionary; - - static GoogleTranslator() { } - private GoogleTranslator() { - _languageDictionary = new Dictionary() { - { "afrikaans", "af"}, - { "albanian", "sq"}, - { "arabic", "ar"}, - { "armenian", "hy"}, - { "azerbaijani", "az"}, - { "basque", "eu"}, - { "belarusian", "be"}, - { "bengali", "bn"}, - { "bulgarian", "bg"}, - { "catalan", "ca"}, - { "chinese-traditional", "zh-TW"}, - { "chinese-simplified", "zh-CN"}, - { "chinese", "zh-CN"}, - { "croatian", "hr"}, - { "czech", "cs"}, - { "danish", "da"}, - { "dutch", "nl"}, - { "english", "en"}, - { "esperanto", "eo"}, - { "estonian", "et"}, - { "filipino", "tl"}, - { "finnish", "fi"}, - { "french", "fr"}, - { "galician", "gl"}, - { "german", "de"}, - { "georgian", "ka"}, - { "greek", "el"}, - { "haitian Creole", "ht"}, - { "hebrew", "iw"}, - { "hindi", "hi"}, - { "hungarian", "hu"}, - { "icelandic", "is"}, - { "indonesian", "id"}, - { "irish", "ga"}, - { "italian", "it"}, - { "japanese", "ja"}, - { "korean", "ko"}, - { "lao", "lo"}, - { "latin", "la"}, - { "latvian", "lv"}, - { "lithuanian", "lt"}, - { "macedonian", "mk"}, - { "malay", "ms"}, - { "maltese", "mt"}, - { "norwegian", "no"}, - { "persian", "fa"}, - { "polish", "pl"}, - { "portuguese", "pt"}, - { "romanian", "ro"}, - { "russian", "ru"}, - { "serbian", "sr"}, - { "slovak", "sk"}, - { "slovenian", "sl"}, - { "spanish", "es"}, - { "swahili", "sw"}, - { "swedish", "sv"}, - { "tamil", "ta"}, - { "telugu", "te"}, - { "thai", "th"}, - { "turkish", "tr"}, - { "ukrainian", "uk"}, - { "urdu", "ur"}, - { "vietnamese", "vi"}, - { "welsh", "cy"}, - { "yiddish", "yi"}, - - { "af", "af"}, - { "sq", "sq"}, - { "ar", "ar"}, - { "hy", "hy"}, - { "az", "az"}, - { "eu", "eu"}, - { "be", "be"}, - { "bn", "bn"}, - { "bg", "bg"}, - { "ca", "ca"}, - { "zh-tw", "zh-TW"}, - { "zh-cn", "zh-CN"}, - { "hr", "hr"}, - { "cs", "cs"}, - { "da", "da"}, - { "nl", "nl"}, - { "en", "en"}, - { "eo", "eo"}, - { "et", "et"}, - { "tl", "tl"}, - { "fi", "fi"}, - { "fr", "fr"}, - { "gl", "gl"}, - { "de", "de"}, - { "ka", "ka"}, - { "el", "el"}, - { "ht", "ht"}, - { "iw", "iw"}, - { "hi", "hi"}, - { "hu", "hu"}, - { "is", "is"}, - { "id", "id"}, - { "ga", "ga"}, - { "it", "it"}, - { "ja", "ja"}, - { "ko", "ko"}, - { "lo", "lo"}, - { "la", "la"}, - { "lv", "lv"}, - { "lt", "lt"}, - { "mk", "mk"}, - { "ms", "ms"}, - { "mt", "mt"}, - { "no", "no"}, - { "fa", "fa"}, - { "pl", "pl"}, - { "pt", "pt"}, - { "ro", "ro"}, - { "ru", "ru"}, - { "sr", "sr"}, - { "sk", "sk"}, - { "sl", "sl"}, - { "es", "es"}, - { "sw", "sw"}, - { "sv", "sv"}, - { "ta", "ta"}, - { "te", "te"}, - { "th", "th"}, - { "tr", "tr"}, - { "uk", "uk"}, - { "ur", "ur"}, - { "vi", "vi"}, - { "cy", "cy"}, - { "yi", "yi"}, - }; - } - - public async Task Translate(string sourceText, string sourceLanguage, string targetLanguage) - { - string text; - - if(!_languageDictionary.ContainsKey(sourceLanguage) || - !_languageDictionary.ContainsKey(targetLanguage)) - throw new ArgumentException(); - - - var url = string.Format("https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}", - ConvertToLanguageCode(sourceLanguage), - ConvertToLanguageCode(targetLanguage), - WebUtility.UrlEncode(sourceText)); - using (var http = new HttpClient()) - { - http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); - text = await http.GetStringAsync(url).ConfigureAwait(false); - } - - return (string.Concat(JArray.Parse(text)[0].Select(x => x[0]))); - } - - private string ConvertToLanguageCode(string language) - { - string mode; - _languageDictionary.TryGetValue(language, out mode); - return mode; - } - } -} diff --git a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs b/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs index a867aeb8..4dab9c28 100644 --- a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs @@ -3,6 +3,7 @@ using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Models; using NadekoBot.Services; +using NadekoBot.Services.Searches; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; @@ -19,27 +20,11 @@ namespace NadekoBot.Modules.Searches [Group] public class JokeCommands : NadekoSubmodule { - private static List wowJokes { get; } = new List(); - private static List magicItems { get; } = new List(); - private new static readonly Logger _log; + private readonly SearchesService _searches; - static JokeCommands() + public JokeCommands(SearchesService searches) { - _log = LogManager.GetCurrentClassLogger(); - - if (File.Exists("data/wowjokes.json")) - { - wowJokes = JsonConvert.DeserializeObject>(File.ReadAllText("data/wowjokes.json")); - } - else - _log.Warn("data/wowjokes.json is missing. WOW Jokes are not loaded."); - - if (File.Exists("data/magicitems.json")) - { - magicItems = JsonConvert.DeserializeObject>(File.ReadAllText("data/magicitems.json")); - } - else - _log.Warn("data/magicitems.json is missing. Magic items are not loaded."); + _searches = searches; } [NadekoCommand, Usage, Description, Aliases] @@ -75,24 +60,24 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task WowJoke() { - if (!wowJokes.Any()) + if (!_searches.WowJokes.Any()) { await ReplyErrorLocalized("jokes_not_loaded").ConfigureAwait(false); return; } - var joke = wowJokes[new NadekoRandom().Next(0, wowJokes.Count)]; + var joke = _searches.WowJokes[new NadekoRandom().Next(0, _searches.WowJokes.Count)]; await Context.Channel.SendConfirmAsync(joke.Question, joke.Answer).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] public async Task MagicItem() { - if (!wowJokes.Any()) + if (!_searches.WowJokes.Any()) { await ReplyErrorLocalized("magicitems_not_loaded").ConfigureAwait(false); return; } - var item = magicItems[new NadekoRandom().Next(0, magicItems.Count)]; + var item = _searches.MagicItems[new NadekoRandom().Next(0, _searches.MagicItems.Count)]; await Context.Channel.SendConfirmAsync("✨" + item.Name, item.Description).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index 83fe18a8..539b11bf 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -22,7 +22,7 @@ namespace NadekoBot.Modules.Searches obj["name"].GetHashCode(); } - private static string[] trashTalk { get; } = { "Better ban your counters. You are going to carry the game anyway.", + private static readonly string[] trashTalk = { "Better ban your counters. You are going to carry the game anyway.", "Go with the flow. Don't think. Just ban one of these.", "DONT READ BELOW! Ban Urgot mid OP 100%. Im smurf Diamond 1.", "Ask your teammates what would they like to play, and ban that.", @@ -40,7 +40,7 @@ namespace NadekoBot.Modules.Searches using (var http = new HttpClient()) { var data = JObject.Parse(await http.GetStringAsync($"http://api.champion.gg/stats/champs/mostBanned?" + - $"api_key={NadekoBot.Credentials.LoLApiKey}&page=1&" + + $"api_key={_creds.LoLApiKey}&page=1&" + $"limit={showCount}") .ConfigureAwait(false))["data"] as JArray; var dataList = data.Distinct(new ChampionNameComparer()).Take(showCount).ToList(); @@ -127,7 +127,7 @@ namespace NadekoBot.Modules.Searches // await e.Channel.SendFile("champ.png", champ.ImageStream).ConfigureAwait(false); // return; // } -// var allData = JArray.Parse(await Classes.http.GetStringAsync($"http://api.champion.gg/champion/{name}?api_key={NadekoBot.Credentials.LOLAPIKey}").ConfigureAwait(false)); +// var allData = JArray.Parse(await Classes.http.GetStringAsync($"http://api.champion.gg/champion/{name}?api_key={_creds.LOLAPIKey}").ConfigureAwait(false)); // JToken data = null; // if (role != null) // { @@ -168,7 +168,7 @@ namespace NadekoBot.Modules.Searches // roles[i] = ">" + roles[i] + "<"; // } // var general = JArray.Parse(await http.GetStringAsync($"http://api.champion.gg/stats/" + -// $"champs/{name}?api_key={NadekoBot.Credentials.LOLAPIKey}") +// $"champs/{name}?api_key={_creds.LOLAPIKey}") // .ConfigureAwait(false)) // .FirstOrDefault(jt => jt["role"].ToString() == role)?["general"]; // if (general == null) diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/ImdbMovie.cs b/src/NadekoBot/Modules/Searches/Commands/Models/ImdbMovie.cs deleted file mode 100644 index 318eb6dc..00000000 --- a/src/NadekoBot/Modules/Searches/Commands/Models/ImdbMovie.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NadekoBot.Extensions; -using System.Collections.Generic; -using System.Net; - -namespace NadekoBot.Modules.Searches.Models -{ - public class ImdbMovie - { - public bool Status { get; set; } - public string Id { get; set; } - public string Title { get; set; } - public string OriginalTitle { get; set; } - public string Year { get; set; } - public string Rating { get; set; } - public string Plot { get; set; } - public string Poster { get; set; } - public List Genres { get; set; } - public string ImdbURL { get; set; } - - public Dictionary Aka { get; set; } - - public override string ToString() => -$@"`Title:` {WebUtility.HtmlDecode(Title)} {(string.IsNullOrEmpty(OriginalTitle) ? "" : $"({OriginalTitle})")} -`Year:` {Year} -`Rating:` {Rating} -`Genre:` {GenresAsString} -`Link:` <{ImdbURL}> -`Plot:` {System.Net.WebUtility.HtmlDecode(Plot.TrimTo(500))} -`Poster:` " + NadekoBot.Google.ShortenUrl(Poster).GetAwaiter().GetResult(); - public string GenresAsString => - string.Join(", ", Genres); - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs index a3d56fcf..dee47aab 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs @@ -1,6 +1,7 @@ using Discord; using Discord.API; using NadekoBot.Extensions; +using NadekoBot.Services; using Newtonsoft.Json; using System; using System.Net.Http; @@ -12,7 +13,7 @@ namespace NadekoBot.Modules.Searches.Commands.OMDB { private const string queryUrl = "http://www.omdbapi.com/?t={0}&y=&plot=full&r=json"; - public static async Task FindMovie(string name) + public static async Task FindMovie(string name, IGoogleApiService google) { using (var http = new HttpClient()) { @@ -20,7 +21,7 @@ namespace NadekoBot.Modules.Searches.Commands.OMDB var movie = JsonConvert.DeserializeObject(res); if (movie?.Title == null) return null; - movie.Poster = await NadekoBot.Google.ShortenUrl(movie.Poster); + movie.Poster = await google.ShortenUrl(movie.Poster); return movie; } } diff --git a/src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs b/src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs index 0a764fab..1fcc789e 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs @@ -2,6 +2,7 @@ using Discord.Commands; using NadekoBot.Attributes; using NadekoBot.Extensions; +using NadekoBot.Services; using Newtonsoft.Json.Linq; using System; using System.Globalization; @@ -17,6 +18,15 @@ namespace NadekoBot.Modules.Searches [Group] public class OsuCommands : NadekoSubmodule { + private readonly IGoogleApiService _google; + private readonly IBotCredentials _creds; + + public OsuCommands(IGoogleApiService google, IBotCredentials creds) + { + _google = google; + _creds = creds; + } + [NadekoCommand, Usage, Description, Aliases] public async Task Osu(string usr, [Remainder] string mode = null) { @@ -51,7 +61,7 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Osub([Remainder] string map) { - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.OsuApiKey)) + if (string.IsNullOrWhiteSpace(_creds.OsuApiKey)) { await ReplyErrorLocalized("osu_api_key").ConfigureAwait(false); return; @@ -65,7 +75,7 @@ namespace NadekoBot.Modules.Searches using (var http = new HttpClient()) { var mapId = ResolveMap(map); - var reqString = $"https://osu.ppy.sh/api/get_beatmaps?k={NadekoBot.Credentials.OsuApiKey}&{mapId}"; + var reqString = $"https://osu.ppy.sh/api/get_beatmaps?k={_creds.OsuApiKey}&{mapId}"; var obj = JArray.Parse(await http.GetStringAsync(reqString).ConfigureAwait(false))[0]; var sb = new System.Text.StringBuilder(); var starRating = Math.Round(double.Parse($"{obj["difficultyrating"]}", CultureInfo.InvariantCulture), 2); @@ -86,7 +96,7 @@ namespace NadekoBot.Modules.Searches public async Task Osu5(string user, [Remainder] string mode = null) { var channel = (ITextChannel)Context.Channel; - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.OsuApiKey)) + if (string.IsNullOrWhiteSpace(_creds.OsuApiKey)) { await channel.SendErrorAsync("An osu! API key is required.").ConfigureAwait(false); return; @@ -107,12 +117,12 @@ namespace NadekoBot.Modules.Searches m = ResolveGameMode(mode); } - var reqString = $"https://osu.ppy.sh/api/get_user_best?k={NadekoBot.Credentials.OsuApiKey}&u={Uri.EscapeDataString(user)}&type=string&limit=5&m={m}"; + var reqString = $"https://osu.ppy.sh/api/get_user_best?k={_creds.OsuApiKey}&u={Uri.EscapeDataString(user)}&type=string&limit=5&m={m}"; var obj = JArray.Parse(await http.GetStringAsync(reqString).ConfigureAwait(false)); var sb = new System.Text.StringBuilder($"`Top 5 plays for {user}:`\n```xl" + Environment.NewLine); foreach (var item in obj) { - var mapReqString = $"https://osu.ppy.sh/api/get_beatmaps?k={NadekoBot.Credentials.OsuApiKey}&b={item["beatmap_id"]}"; + var mapReqString = $"https://osu.ppy.sh/api/get_beatmaps?k={_creds.OsuApiKey}&b={item["beatmap_id"]}"; var map = JArray.Parse(await http.GetStringAsync(mapReqString).ConfigureAwait(false))[0]; var pp = Math.Round(double.Parse($"{item["pp"]}", CultureInfo.InvariantCulture), 2); var acc = CalculateAcc(item, m); diff --git a/src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs b/src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs index b44eb97d..d9d26701 100644 --- a/src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs @@ -29,7 +29,7 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Placelist() { - await Context.Channel.SendConfirmAsync(GetText("list_of_place_tags", NadekoBot.ModulePrefixes[typeof(Searches).Name]), + await Context.Channel.SendConfirmAsync(GetText("list_of_place_tags", NadekoBot.Prefix), typesStr) .ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs index bc77cda3..7a24c20f 100644 --- a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs @@ -3,6 +3,7 @@ using Discord.Commands; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Models; +using NadekoBot.Services.Searches; using Newtonsoft.Json; using NLog; using System.Collections.Generic; @@ -17,28 +18,14 @@ namespace NadekoBot.Modules.Searches [Group] public class PokemonSearchCommands : NadekoSubmodule { - private static Dictionary pokemons { get; } = new Dictionary(); - private static Dictionary pokemonAbilities { get; } = new Dictionary(); + private readonly SearchesService _searches; - public const string PokemonAbilitiesFile = "data/pokemon/pokemon_abilities7.json"; + public Dictionary Pokemons => _searches.Pokemons; + public Dictionary PokemonAbilities => _searches.PokemonAbilities; - public const string PokemonListFile = "data/pokemon/pokemon_list7.json"; - private new static readonly Logger _log; - - static PokemonSearchCommands() + public PokemonSearchCommands(SearchesService searches) { - _log = LogManager.GetCurrentClassLogger(); - - if (File.Exists(PokemonListFile)) - { - pokemons = JsonConvert.DeserializeObject>(File.ReadAllText(PokemonListFile)); - } - else - _log.Warn(PokemonListFile + " is missing. Pokemon abilities not loaded."); - if (File.Exists(PokemonAbilitiesFile)) - pokemonAbilities = JsonConvert.DeserializeObject>(File.ReadAllText(PokemonAbilitiesFile)); - else - _log.Warn(PokemonAbilitiesFile + " is missing. Pokemon abilities not loaded."); + _searches = searches; } [NadekoCommand, Usage, Description, Aliases] @@ -48,7 +35,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(pokemon)) return; - foreach (var kvp in pokemons) + foreach (var kvp in Pokemons) { if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant()) { @@ -71,7 +58,7 @@ namespace NadekoBot.Modules.Searches ability = ability?.Trim().ToUpperInvariant().Replace(" ", ""); if (string.IsNullOrWhiteSpace(ability)) return; - foreach (var kvp in pokemonAbilities) + foreach (var kvp in PokemonAbilities) { if (kvp.Key.ToUpperInvariant() == ability) { diff --git a/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs index d122e47a..5ad47ffd 100644 --- a/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs @@ -1,195 +1,30 @@ using Discord.Commands; using Discord; -using System; -using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using NadekoBot.Services; using System.Threading; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using System.Net.Http; using NadekoBot.Attributes; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; using NadekoBot.Extensions; +using NadekoBot.Services.Searches; namespace NadekoBot.Modules.Searches { public partial class Searches { - public class StreamStatus - { - public bool IsLive { get; set; } - public string ApiLink { get; set; } - public string Views { get; set; } - } - - public class HitboxResponse { - public bool Success { get; set; } = true; - [JsonProperty("media_is_live")] - public string MediaIsLive { get; set; } - public bool IsLive => MediaIsLive == "1"; - [JsonProperty("media_views")] - public string Views { get; set; } - } - - public class TwitchResponse - { - public string Error { get; set; } = null; - public bool IsLive => Stream != null; - public StreamInfo Stream { get; set; } - - public class StreamInfo - { - public int Viewers { get; set; } - } - } - - public class BeamResponse - { - public string Error { get; set; } = null; - - [JsonProperty("online")] - public bool IsLive { get; set; } - public int ViewersCurrent { get; set; } - } - - public class StreamNotFoundException : Exception - { - public StreamNotFoundException(string message) : base("Stream '" + message + "' not found.") - { - } - } - [Group] public class StreamNotificationCommands : NadekoSubmodule { - private static readonly Timer _checkTimer; - private static readonly ConcurrentDictionary _cachedStatuses = new ConcurrentDictionary(); + private readonly DbHandler _db; + private readonly StreamNotificationService _service; - private static bool firstPass { get; set; } = true; - - static StreamNotificationCommands() + public StreamNotificationCommands(DbHandler db, StreamNotificationService service) { - _checkTimer = new Timer(async (state) => - { - var oldCachedStatuses = new ConcurrentDictionary(_cachedStatuses); - _cachedStatuses.Clear(); - IEnumerable streams; - using (var uow = DbHandler.UnitOfWork()) - { - streams = uow.GuildConfigs.GetAllFollowedStreams(); - } - - await Task.WhenAll(streams.Select(async fs => - { - try - { - var newStatus = await GetStreamStatus(fs).ConfigureAwait(false); - if (firstPass) - { - return; - } - - StreamStatus oldStatus; - if (oldCachedStatuses.TryGetValue(newStatus.ApiLink, out oldStatus) && - oldStatus.IsLive != newStatus.IsLive) - { - var server = NadekoBot.Client.GetGuild(fs.GuildId); - var channel = server?.GetTextChannel(fs.ChannelId); - if (channel == null) - return; - try - { - await channel.EmbedAsync(fs.GetEmbed(newStatus, channel.Guild.Id)).ConfigureAwait(false); - } - catch - { - // ignored - } - } - } - catch - { - // ignored - } - })); - - firstPass = false; - }, null, TimeSpan.Zero, TimeSpan.FromSeconds(60)); - } - - private static async Task GetStreamStatus(FollowedStream stream, bool checkCache = true) - { - string response; - StreamStatus result; - switch (stream.Type) - { - case FollowedStream.FollowedStreamType.Hitbox: - var hitboxUrl = $"https://api.hitbox.tv/media/status/{stream.Username.ToLowerInvariant()}"; - if (checkCache && _cachedStatuses.TryGetValue(hitboxUrl, out result)) - return result; - using (var http = new HttpClient()) - { - response = await http.GetStringAsync(hitboxUrl).ConfigureAwait(false); - } - var hbData = JsonConvert.DeserializeObject(response); - if (!hbData.Success) - throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]"); - result = new StreamStatus() - { - IsLive = hbData.IsLive, - ApiLink = hitboxUrl, - Views = hbData.Views - }; - _cachedStatuses.AddOrUpdate(hitboxUrl, result, (key, old) => result); - return result; - case FollowedStream.FollowedStreamType.Twitch: - var twitchUrl = $"https://api.twitch.tv/kraken/streams/{Uri.EscapeUriString(stream.Username.ToLowerInvariant())}?client_id=67w6z9i09xv2uoojdm9l0wsyph4hxo6"; - if (checkCache && _cachedStatuses.TryGetValue(twitchUrl, out result)) - return result; - using (var http = new HttpClient()) - { - response = await http.GetStringAsync(twitchUrl).ConfigureAwait(false); - } - var twData = JsonConvert.DeserializeObject(response); - if (twData.Error != null) - { - throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]"); - } - result = new StreamStatus() - { - IsLive = twData.IsLive, - ApiLink = twitchUrl, - Views = twData.Stream?.Viewers.ToString() ?? "0" - }; - _cachedStatuses.AddOrUpdate(twitchUrl, result, (key, old) => result); - return result; - case FollowedStream.FollowedStreamType.Beam: - var beamUrl = $"https://beam.pro/api/v1/channels/{stream.Username.ToLowerInvariant()}"; - if (checkCache && _cachedStatuses.TryGetValue(beamUrl, out result)) - return result; - using (var http = new HttpClient()) - { - response = await http.GetStringAsync(beamUrl).ConfigureAwait(false); - } - - var bmData = JsonConvert.DeserializeObject(response); - if (bmData.Error != null) - throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]"); - result = new StreamStatus() - { - IsLive = bmData.IsLive, - ApiLink = beamUrl, - Views = bmData.ViewersCurrent.ToString() - }; - _cachedStatuses.AddOrUpdate(beamUrl, result, (key, old) => result); - return result; - default: - break; - } - return null; + _db = db; + _service = service; } [NadekoCommand, Usage, Description, Aliases] @@ -218,7 +53,7 @@ namespace NadekoBot.Modules.Searches public async Task ListStreams() { IEnumerable streams; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { streams = uow.GuildConfigs .For(Context.Guild.Id, @@ -260,7 +95,7 @@ namespace NadekoBot.Modules.Searches }; bool removed; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var config = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(gc => gc.FollowedStreams)); removed = config.FollowedStreams.Remove(fs); @@ -287,7 +122,7 @@ namespace NadekoBot.Modules.Searches return; try { - var streamStatus = (await GetStreamStatus(new FollowedStream + var streamStatus = (await _service.GetStreamStatus(new FollowedStream { Username = stream, Type = platform, @@ -325,7 +160,7 @@ namespace NadekoBot.Modules.Searches StreamStatus status; try { - status = await GetStreamStatus(fs).ConfigureAwait(false); + status = await _service.GetStreamStatus(fs).ConfigureAwait(false); } catch { @@ -333,53 +168,15 @@ namespace NadekoBot.Modules.Searches return; } - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.GuildConfigs.For(channel.Guild.Id, set => set.Include(gc => gc.FollowedStreams)) .FollowedStreams .Add(fs); await uow.CompleteAsync().ConfigureAwait(false); } - await channel.EmbedAsync(fs.GetEmbed(status, Context.Guild.Id), GetText("stream_tracked")).ConfigureAwait(false); + await channel.EmbedAsync(_service.GetEmbed(fs, status, Context.Guild.Id), GetText("stream_tracked")).ConfigureAwait(false); } } } - - public static class FollowedStreamExtensions - { - public static EmbedBuilder GetEmbed(this FollowedStream fs, Searches.StreamStatus status, ulong guildId) - { - var embed = new EmbedBuilder().WithTitle(fs.Username) - .WithUrl(fs.GetLink()) - .AddField(efb => efb.WithName(fs.GetText("status")) - .WithValue(status.IsLive ? "Online" : "Offline") - .WithIsInline(true)) - .AddField(efb => efb.WithName(fs.GetText("viewers")) - .WithValue(status.IsLive ? status.Views : "-") - .WithIsInline(true)) - .AddField(efb => efb.WithName(fs.GetText("platform")) - .WithValue(fs.Type.ToString()) - .WithIsInline(true)) - .WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor); - - return embed; - } - - public static string GetText(this FollowedStream fs, string key, params object[] replacements) => - NadekoTopLevelModule.GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(fs.GuildId), - typeof(Searches).Name.ToLowerInvariant(), - replacements); - - public static string GetLink(this FollowedStream fs) - { - if (fs.Type == FollowedStream.FollowedStreamType.Hitbox) - return $"http://www.hitbox.tv/{fs.Username}/"; - if (fs.Type == FollowedStream.FollowedStreamType.Twitch) - return $"http://www.twitch.tv/{fs.Username}/"; - if (fs.Type == FollowedStream.FollowedStreamType.Beam) - return $"https://beam.pro/{fs.Username}/"; - return "??"; - } - } } diff --git a/src/NadekoBot/Modules/Searches/Commands/Translator.cs b/src/NadekoBot/Modules/Searches/Commands/Translator.cs index 9ed33ce5..6db98c67 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Translator.cs +++ b/src/NadekoBot/Modules/Searches/Commands/Translator.cs @@ -7,54 +7,23 @@ using System.Threading.Tasks; using System.Collections.Concurrent; using System.Linq; using Discord.WebSocket; +using NadekoBot.Services.Searches; +using NadekoBot.Services; namespace NadekoBot.Modules.Searches { public partial class Searches { - public struct UserChannelPair - { - public ulong UserId { get; set; } - public ulong ChannelId { get; set; } - } - [Group] public class TranslateCommands : NadekoSubmodule { - private static ConcurrentDictionary translatedChannels { get; } = new ConcurrentDictionary(); - private static ConcurrentDictionary userLanguages { get; } = new ConcurrentDictionary(); + private readonly SearchesService _searches; + private readonly IGoogleApiService _google; - static TranslateCommands() + public TranslateCommands(SearchesService searches, IGoogleApiService google) { - NadekoBot.Client.MessageReceived += async (msg) => - { - try - { - var umsg = msg as SocketUserMessage; - if (umsg == null) - return; - - bool autoDelete; - if (!translatedChannels.TryGetValue(umsg.Channel.Id, out autoDelete)) - return; - var key = new UserChannelPair() - { - UserId = umsg.Author.Id, - ChannelId = umsg.Channel.Id, - }; - - string langs; - if (!userLanguages.TryGetValue(key, out langs)) - return; - - var text = await TranslateInternal(langs, umsg.Resolve(TagHandling.Ignore)) - .ConfigureAwait(false); - if (autoDelete) - try { await umsg.DeleteAsync().ConfigureAwait(false); } catch { } - await umsg.Channel.SendConfirmAsync($"{umsg.Author.Mention} `:` " + text.Replace("<@ ", "<@").Replace("<@! ", "<@!")).ConfigureAwait(false); - } - catch { } - }; + _searches = searches; + _google = google; } [NadekoCommand, Usage, Description, Aliases] @@ -63,7 +32,7 @@ namespace NadekoBot.Modules.Searches try { await Context.Channel.TriggerTypingAsync().ConfigureAwait(false); - var translation = await TranslateInternal(langs, text); + var translation = await _searches.Translate(langs, text); await Context.Channel.SendConfirmAsync(GetText("translation") + " " + langs, translation).ConfigureAwait(false); } catch @@ -72,19 +41,6 @@ namespace NadekoBot.Modules.Searches } } - private static async Task TranslateInternal(string langs, [Remainder] string text = null) - { - var langarr = langs.ToLowerInvariant().Split('>'); - if (langarr.Length != 2) - throw new ArgumentException(); - var from = langarr[0]; - var to = langarr[1]; - text = text?.Trim(); - if (string.IsNullOrWhiteSpace(text)) - throw new ArgumentException(); - return (await GoogleTranslator.Instance.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(); - } - public enum AutoDeleteAutoTranslate { Del, @@ -101,18 +57,17 @@ namespace NadekoBot.Modules.Searches if (autoDelete == AutoDeleteAutoTranslate.Del) { - translatedChannels.AddOrUpdate(channel.Id, true, (key, val) => true); + _searches.TranslatedChannels.AddOrUpdate(channel.Id, true, (key, val) => true); await ReplyConfirmLocalized("atl_ad_started").ConfigureAwait(false); return; } - - bool throwaway; - if (translatedChannels.TryRemove(channel.Id, out throwaway)) + + if (_searches.TranslatedChannels.TryRemove(channel.Id, out var throwaway)) { await ReplyConfirmLocalized("atl_stopped").ConfigureAwait(false); return; } - if (translatedChannels.TryAdd(channel.Id, autoDelete == AutoDeleteAutoTranslate.Del)) + if (_searches.TranslatedChannels.TryAdd(channel.Id, autoDelete == AutoDeleteAutoTranslate.Del)) { await ReplyConfirmLocalized("atl_started").ConfigureAwait(false); } @@ -130,7 +85,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(langs)) { - if (userLanguages.TryRemove(ucp, out langs)) + if (_searches.UserLanguages.TryRemove(ucp, out langs)) await ReplyConfirmLocalized("atl_removed").ConfigureAwait(false); return; } @@ -141,13 +96,13 @@ namespace NadekoBot.Modules.Searches var from = langarr[0]; var to = langarr[1]; - if (!GoogleTranslator.Instance.Languages.Contains(from) || !GoogleTranslator.Instance.Languages.Contains(to)) + if (!_google.Languages.Contains(from) || !_google.Languages.Contains(to)) { await ReplyErrorLocalized("invalid_lang").ConfigureAwait(false); return; } - userLanguages.AddOrUpdate(ucp, langs, (key, val) => langs); + _searches.UserLanguages.AddOrUpdate(ucp, langs, (key, val) => langs); await ReplyConfirmLocalized("atl_set", from, to).ConfigureAwait(false); } @@ -156,7 +111,7 @@ namespace NadekoBot.Modules.Searches [RequireContext(ContextType.Guild)] public async Task Translangs() { - await Context.Channel.SendTableAsync(GoogleTranslator.Instance.Languages, str => $"{str,-15}", 3); + await Context.Channel.SendTableAsync(_google.Languages, str => $"{str,-15}", 3); } } diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index 1c78855f..9acb7391 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Linq; -using System.Text; using System.Net.Http; using NadekoBot.Services; using System.Threading.Tasks; @@ -17,18 +16,27 @@ using NadekoBot.Modules.Searches.Commands.Models; using AngleSharp; using AngleSharp.Dom.Html; using AngleSharp.Dom; -using System.Xml; using Configuration = AngleSharp.Configuration; using NadekoBot.Attributes; using Discord.Commands; -using ImageSharp.Processing.Processors; using ImageSharp; +using NadekoBot.Services.Searches; namespace NadekoBot.Modules.Searches { - [NadekoModule("Searches", "~")] public partial class Searches : NadekoTopLevelModule { + private readonly IBotCredentials _creds; + private readonly IGoogleApiService _google; + private readonly SearchesService _searches; + + public Searches(IBotCredentials creds, IGoogleApiService google, SearchesService searches) + { + _creds = creds; + _google = google; + _searches = searches; + } + [NadekoCommand, Usage, Description, Aliases] public async Task Weather([Remainder] string query) { @@ -59,16 +67,16 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Time([Remainder] string arg) { - if (string.IsNullOrWhiteSpace(arg) || string.IsNullOrWhiteSpace(NadekoBot.Credentials.GoogleApiKey)) + if (string.IsNullOrWhiteSpace(arg) || string.IsNullOrWhiteSpace(_creds.GoogleApiKey)) return; using (var http = new HttpClient()) { - var res = await http.GetStringAsync($"https://maps.googleapis.com/maps/api/geocode/json?address={arg}&key={NadekoBot.Credentials.GoogleApiKey}").ConfigureAwait(false); + var res = await http.GetStringAsync($"https://maps.googleapis.com/maps/api/geocode/json?address={arg}&key={_creds.GoogleApiKey}").ConfigureAwait(false); var obj = JsonConvert.DeserializeObject(res); var currentSeconds = DateTime.UtcNow.UnixTimestamp(); - var timeRes = await http.GetStringAsync($"https://maps.googleapis.com/maps/api/timezone/json?location={obj.results[0].Geometry.Location.Lat},{obj.results[0].Geometry.Location.Lng}×tamp={currentSeconds}&key={NadekoBot.Credentials.GoogleApiKey}").ConfigureAwait(false); + var timeRes = await http.GetStringAsync($"https://maps.googleapis.com/maps/api/timezone/json?location={obj.results[0].Geometry.Location.Lat},{obj.results[0].Geometry.Location.Lng}×tamp={currentSeconds}&key={_creds.GoogleApiKey}").ConfigureAwait(false); var timeObj = JsonConvert.DeserializeObject(timeRes); var time = DateTime.UtcNow.AddSeconds(timeObj.DstOffset + timeObj.RawOffset); @@ -81,7 +89,7 @@ namespace NadekoBot.Modules.Searches public async Task Youtube([Remainder] string query = null) { if (!await ValidateQuery(Context.Channel, query).ConfigureAwait(false)) return; - var result = (await NadekoBot.Google.GetVideosByKeywordsAsync(query, 1)).FirstOrDefault(); + var result = (await _google.GetVideosByKeywordsAsync(query, 1)).FirstOrDefault(); if (string.IsNullOrWhiteSpace(result)) { await ReplyErrorLocalized("no_results").ConfigureAwait(false); @@ -97,7 +105,7 @@ namespace NadekoBot.Modules.Searches if (!(await ValidateQuery(Context.Channel, query).ConfigureAwait(false))) return; await Context.Channel.TriggerTypingAsync().ConfigureAwait(false); - var movie = await OmdbProvider.FindMovie(query); + var movie = await OmdbProvider.FindMovie(query, _google); if (movie == null) { await ReplyErrorLocalized("imdb_fail").ConfigureAwait(false); @@ -137,7 +145,7 @@ namespace NadekoBot.Modules.Searches try { - var res = await NadekoBot.Google.GetImageAsync(terms).ConfigureAwait(false); + var res = await _google.GetImageAsync(terms).ConfigureAwait(false); var embed = new EmbedBuilder() .WithOkColor() .WithAuthor(eab => eab.WithName(GetText("image_search_for") + " " + terms.TrimTo(50)) @@ -189,7 +197,7 @@ namespace NadekoBot.Modules.Searches terms = WebUtility.UrlEncode(terms).Replace(' ', '+'); try { - var res = await NadekoBot.Google.GetImageAsync(terms, new NadekoRandom().Next(0, 50)).ConfigureAwait(false); + var res = await _google.GetImageAsync(terms, new NadekoRandom().Next(0, 50)).ConfigureAwait(false); var embed = new EmbedBuilder() .WithOkColor() .WithAuthor(eab => eab.WithName(GetText("image_search_for") + " " + terms.TrimTo(50)) @@ -239,7 +247,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(ffs)) return; - await Context.Channel.SendConfirmAsync("<" + await NadekoBot.Google.ShortenUrl($"http://lmgtfy.com/?q={ Uri.EscapeUriString(ffs) }") + ">") + await Context.Channel.SendConfirmAsync("<" + await _google.ShortenUrl($"http://lmgtfy.com/?q={ Uri.EscapeUriString(ffs) }") + ">") .ConfigureAwait(false); } @@ -249,7 +257,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(arg)) return; - var shortened = await NadekoBot.Google.ShortenUrl(arg).ConfigureAwait(false); + var shortened = await _google.ShortenUrl(arg).ConfigureAwait(false); if (shortened == arg) { @@ -315,7 +323,7 @@ namespace NadekoBot.Modules.Searches .WithFooter(efb => efb.WithText(totalResults)); var desc = await Task.WhenAll(results.Select(async res => - $"[{Format.Bold(res?.Title)}]({(await NadekoBot.Google.ShortenUrl(res?.Link))})\n{res?.Text}\n\n")) + $"[{Format.Bold(res?.Title)}]({(await _google.ShortenUrl(res?.Link))})\n{res?.Text}\n\n")) .ConfigureAwait(false); await Context.Channel.EmbedAsync(embed.WithDescription(string.Concat(desc))).ConfigureAwait(false); } @@ -339,7 +347,7 @@ namespace NadekoBot.Modules.Searches if (items == null || items.Length == 0) throw new KeyNotFoundException("Cannot find a card by that name"); var item = items[new NadekoRandom().Next(0, items.Length)]; - var storeUrl = await NadekoBot.Google.ShortenUrl(item["store_url"].ToString()); + var storeUrl = await _google.ShortenUrl(item["store_url"].ToString()); var cost = item["cost"].ToString(); var desc = item["text"].ToString(); var types = string.Join(",\n", item["types"].ToObject()); @@ -351,7 +359,7 @@ namespace NadekoBot.Modules.Searches .AddField(efb => efb.WithName(GetText("store_url")).WithValue(storeUrl).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("cost")).WithValue(cost).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("types")).WithValue(types).WithIsInline(true)); - //.AddField(efb => efb.WithName("Store Url").WithValue(await NadekoBot.Google.ShortenUrl(items[0]["store_url"].ToString())).WithIsInline(true)); + //.AddField(efb => efb.WithName("Store Url").WithValue(await _google.ShortenUrl(items[0]["store_url"].ToString())).WithIsInline(true)); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -369,7 +377,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(arg)) return; - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.MashapeKey)) + if (string.IsNullOrWhiteSpace(_creds.MashapeKey)) { await ReplyErrorLocalized("mashape_api_missing").ConfigureAwait(false); return; @@ -379,7 +387,7 @@ namespace NadekoBot.Modules.Searches using (var http = new HttpClient()) { http.DefaultRequestHeaders.Clear(); - http.DefaultRequestHeaders.Add("X-Mashape-Key", NadekoBot.Credentials.MashapeKey); + http.DefaultRequestHeaders.Add("X-Mashape-Key", _creds.MashapeKey); var response = await http.GetStringAsync($"https://omgvamp-hearthstone-v1.p.mashape.com/cards/search/{Uri.EscapeUriString(arg)}") .ConfigureAwait(false); try @@ -422,7 +430,7 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Yodify([Remainder] string query = null) { - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.MashapeKey)) + if (string.IsNullOrWhiteSpace(_creds.MashapeKey)) { await ReplyErrorLocalized("mashape_api_missing").ConfigureAwait(false); return; @@ -435,7 +443,7 @@ namespace NadekoBot.Modules.Searches using (var http = new HttpClient()) { http.DefaultRequestHeaders.Clear(); - http.DefaultRequestHeaders.Add("X-Mashape-Key", NadekoBot.Credentials.MashapeKey); + http.DefaultRequestHeaders.Add("X-Mashape-Key", _creds.MashapeKey); http.DefaultRequestHeaders.Add("Accept", "text/plain"); var res = await http.GetStringAsync($"https://yoda.p.mashape.com/yoda?sentence={Uri.EscapeUriString(query)}").ConfigureAwait(false); try @@ -457,7 +465,7 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task UrbanDict([Remainder] string query = null) { - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.MashapeKey)) + if (string.IsNullOrWhiteSpace(_creds.MashapeKey)) { await ReplyErrorLocalized("mashape_api_missing").ConfigureAwait(false); return; @@ -531,7 +539,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(query)) return; - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.MashapeKey)) + if (string.IsNullOrWhiteSpace(_creds.MashapeKey)) { await ReplyErrorLocalized("mashape_api_missing").ConfigureAwait(false); return; @@ -542,7 +550,7 @@ namespace NadekoBot.Modules.Searches using (var http = new HttpClient()) { http.DefaultRequestHeaders.Clear(); - http.DefaultRequestHeaders.Add("X-Mashape-Key", NadekoBot.Credentials.MashapeKey); + http.DefaultRequestHeaders.Add("X-Mashape-Key", _creds.MashapeKey); res = await http.GetStringAsync($"https://tagdef.p.mashape.com/one.{Uri.EscapeUriString(query)}.json").ConfigureAwait(false); } @@ -655,7 +663,7 @@ namespace NadekoBot.Modules.Searches usr = (IGuildUser)Context.User; var avatarUrl = usr.RealAvatarUrl(); - var shortenedAvatarUrl = await NadekoBot.Google.ShortenUrl(avatarUrl).ConfigureAwait(false); + var shortenedAvatarUrl = await _google.ShortenUrl(avatarUrl).ConfigureAwait(false); await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .AddField(efb => efb.WithName("Username").WithValue(usr.ToString()).WithIsInline(false)) .AddField(efb => efb.WithName("Avatar Url").WithValue(shortenedAvatarUrl).WithIsInline(false)) @@ -682,7 +690,7 @@ namespace NadekoBot.Modules.Searches var found = items["items"][0]; var response = $@"`{GetText("title")}` {found["title"]} `{GetText("quality")}` {found["quality"]} -`{GetText("url")}:` {await NadekoBot.Google.ShortenUrl(found["url"].ToString()).ConfigureAwait(false)}"; +`{GetText("url")}:` {await _google.ShortenUrl(found["url"].ToString()).ConfigureAwait(false)}"; await Context.Channel.SendMessageAsync(response).ConfigureAwait(false); } catch @@ -769,7 +777,7 @@ namespace NadekoBot.Modules.Searches tag = tag?.Trim() ?? ""; - var url = await InternalDapiSearch(tag, type).ConfigureAwait(false); + var url = await _searches.DapiSearch(tag, type).ConfigureAwait(false); if (url == null) await channel.SendErrorAsync(umsg.Author.Mention + " " + GetText("no_results")); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index ebacc438..bb5f9e74 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -21,6 +21,8 @@ using NadekoBot.Services.Searches; using NadekoBot.Services.ClashOfClans; using NadekoBot.Services.Music; using NadekoBot.Services.CustomReactions; +using NadekoBot.Services.Games; +using NadekoBot.Services.Administration; namespace NadekoBot { @@ -73,11 +75,10 @@ namespace NadekoBot }); var google = new GoogleApiService(credentials); - var strings = new NadekoStrings(); - - var greetSettingsService = new GreetSettingsService(AllGuildConfigs, db); - var localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), db); + var strings = new NadekoStrings(localization); + + var greetSettingsService = new GreetSettingsService(Client, AllGuildConfigs, db); var commandService = new CommandService(new CommandServiceConfig() { @@ -97,10 +98,18 @@ namespace NadekoBot //module services var utilityService = new UtilityService(AllGuildConfigs, Client, BotConfig, db); - var searchesService = new SearchesService(); + var searchesService = new SearchesService(Client, google, db); var clashService = new ClashOfClansService(Client, db, localization, strings); var musicService = new MusicService(google, strings, localization, db, soundcloud, credentials); var crService = new CustomReactionsService(db, Client); + var gamesService = new GamesService(Client, BotConfig, AllGuildConfigs, strings, images); +#region administration + var administrationService = new AdministrationService(AllGuildConfigs, commandHandler); + var selfService = new SelfService(this, commandHandler, db, BotConfig); + var vcRoleService = new VcRoleService(Client, AllGuildConfigs); + var vPlusTService = new VplusTService(Client, AllGuildConfigs, strings, db); +#endregion + //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() //todo all Adds should be interfaces @@ -124,6 +133,10 @@ namespace NadekoBot .Add(musicService) .Add(greetSettingsService) .Add(crService) + .Add(gamesService) + .Add(selfService) + .Add(vcRoleService) + .Add(vPlusTService) .Build(); commandHandler.AddServices(Services); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 51178169..5fb0e42b 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -29,21 +29,22 @@ - - - - - - + + + + + + + PreserveNewest diff --git a/src/NadekoBot/Services/Administration/AdministrationService.cs b/src/NadekoBot/Services/Administration/AdministrationService.cs new file mode 100644 index 00000000..69dfaf16 --- /dev/null +++ b/src/NadekoBot/Services/Administration/AdministrationService.cs @@ -0,0 +1,49 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class AdministrationService + { + public readonly ConcurrentHashSet DeleteMessagesOnCommand; + private readonly Logger _log; + + public AdministrationService(IEnumerable gcs, CommandHandler cmdHandler) + { + _log = LogManager.GetCurrentClassLogger(); + + DeleteMessagesOnCommand = new ConcurrentHashSet(gcs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId)); + cmdHandler.CommandExecuted += DelMsgOnCmd_Handler; + } + + private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd) + { + var _ = Task.Run(async () => + { + try + { + var channel = msg.Channel as SocketTextChannel; + if (channel == null) + return; + if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick") + await msg.DeleteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn("Delmsgoncmd errored..."); + _log.Warn(ex); + } + }); + return Task.CompletedTask; + } + } +} diff --git a/src/NadekoBot/Services/Administration/MuteService.cs b/src/NadekoBot/Services/Administration/MuteService.cs new file mode 100644 index 00000000..4f82ee03 --- /dev/null +++ b/src/NadekoBot/Services/Administration/MuteService.cs @@ -0,0 +1,275 @@ +using Discord; +using Discord.WebSocket; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class MuteService + { + public ConcurrentDictionary GuildMuteRoles { get; } + public ConcurrentDictionary> MutedUsers { get; } + public ConcurrentDictionary> UnmuteTimers { get; } + = new ConcurrentDictionary>(); + + public event Action UserMuted = delegate { }; + public event Action UserUnmuted = delegate { }; + + private static readonly OverwritePermissions denyOverwrite = new OverwritePermissions(sendMessages: PermValue.Deny, attachFiles: PermValue.Deny); + + private readonly Logger _log = LogManager.GetCurrentClassLogger(); + private readonly DiscordShardedClient _client; + private readonly DbHandler _db; + + public MuteService(DiscordShardedClient client, IEnumerable gcs, DbHandler db) + { + _client = client; + _db = db; + + GuildMuteRoles = new ConcurrentDictionary(gcs + .Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName)) + .ToDictionary(c => c.GuildId, c => c.MuteRoleName)); + + MutedUsers = new ConcurrentDictionary>(gcs.ToDictionary( + k => k.GuildId, + v => new ConcurrentHashSet(v.MutedUsers.Select(m => m.UserId)) + )); + + foreach (var conf in gcs) + { + foreach (var x in conf.UnmuteTimers) + { + TimeSpan after; + if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) + { + after = TimeSpan.FromMinutes(2); + } + else + { + after = x.UnmuteAt - DateTime.UtcNow; + } + StartUnmuteTimer(conf.GuildId, x.UserId, after); + } + } + + _client.UserJoined += Client_UserJoined; + } + + private async Task Client_UserJoined(IGuildUser usr) + { + try + { + MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted); + + if (muted == null || !muted.Contains(usr.Id)) + return; + await MuteUser(usr).ConfigureAwait(false); + } + catch (Exception ex) + { + LogManager.GetCurrentClassLogger().Warn(ex); + } + } + + public async Task MuteUser(IGuildUser usr, MuteType type = MuteType.All) + { + if (type == MuteType.All) + { + await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); + var muteRole = await GetMuteRole(usr.Guild); + if (!usr.RoleIds.Contains(muteRole.Id)) + await usr.AddRoleAsync(muteRole).ConfigureAwait(false); + StopUnmuteTimer(usr.GuildId, usr.Id); + using (var uow = _db.UnitOfWork) + { + var config = uow.GuildConfigs.For(usr.Guild.Id, + set => set.Include(gc => gc.MutedUsers) + .Include(gc => gc.UnmuteTimers)); + config.MutedUsers.Add(new MutedUserId() + { + UserId = usr.Id + }); + if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted)) + muted.Add(usr.Id); + + config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); + + await uow.CompleteAsync().ConfigureAwait(false); + } + UserMuted(usr, MuteType.All); + } + else if (type == MuteType.Voice) + { + await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); + UserMuted(usr, MuteType.Voice); + } + else if (type == MuteType.Chat) + { + await usr.AddRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); + UserMuted(usr, MuteType.Chat); + } + } + + public async Task UnmuteUser(IGuildUser usr, MuteType type = MuteType.All) + { + if (type == MuteType.All) + { + StopUnmuteTimer(usr.GuildId, usr.Id); + try { await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); } catch { } + try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild)).ConfigureAwait(false); } catch { /*ignore*/ } + using (var uow = _db.UnitOfWork) + { + var config = uow.GuildConfigs.For(usr.Guild.Id, set => set.Include(gc => gc.MutedUsers) + .Include(gc => gc.UnmuteTimers)); + config.MutedUsers.Remove(new MutedUserId() + { + UserId = usr.Id + }); + if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted)) + muted.TryRemove(usr.Id); + + config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); + + await uow.CompleteAsync().ConfigureAwait(false); + } + UserUnmuted(usr, MuteType.All); + } + else if (type == MuteType.Voice) + { + await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); + UserUnmuted(usr, MuteType.Voice); + } + else if (type == MuteType.Chat) + { + await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); + UserUnmuted(usr, MuteType.Chat); + } + } + + public async Task GetMuteRole(IGuild guild) + { + const string defaultMuteRoleName = "nadeko-mute"; + + var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName); + + var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName); + if (muteRole == null) + { + + //if it doesn't exist, create it + try { muteRole = await guild.CreateRoleAsync(muteRoleName, GuildPermissions.None).ConfigureAwait(false); } + catch + { + //if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one + muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) ?? + await guild.CreateRoleAsync(defaultMuteRoleName, GuildPermissions.None).ConfigureAwait(false); + } + } + + foreach (var toOverwrite in (await guild.GetTextChannelsAsync())) + { + try + { + if (!toOverwrite.PermissionOverwrites.Select(x => x.Permissions).Contains(denyOverwrite)) + { + await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite) + .ConfigureAwait(false); + + await Task.Delay(200).ConfigureAwait(false); + } + } + catch + { + // ignored + } + } + + return muteRole; + } + + public async Task TimedMute(IGuildUser user, TimeSpan after) + { + await MuteUser(user).ConfigureAwait(false); // mute the user. This will also remove any previous unmute timers + using (var uow = _db.UnitOfWork) + { + var config = uow.GuildConfigs.For(user.GuildId, set => set.Include(x => x.UnmuteTimers)); + config.UnmuteTimers.Add(new UnmuteTimer() + { + UserId = user.Id, + UnmuteAt = DateTime.UtcNow + after, + }); // add teh unmute timer to the database + uow.Complete(); + } + StartUnmuteTimer(user.GuildId, user.Id, after); // start the timer + } + + public void StartUnmuteTimer(ulong guildId, ulong userId, TimeSpan after) + { + //load the unmute timers for this guild + var userUnmuteTimers = UnmuteTimers.GetOrAdd(guildId, new ConcurrentDictionary()); + + //unmute timer to be added + var toAdd = new Timer(async _ => + { + try + { + var guild = _client.GetGuild(guildId); // load the guild + if (guild == null) + { + RemoveUnmuteTimerFromDb(guildId, userId); + return; // if guild can't be found, just remove the timer from db + } + // unmute the user, this will also remove the timer from the db + await UnmuteUser(guild.GetUser(userId)).ConfigureAwait(false); + } + catch (Exception ex) + { + RemoveUnmuteTimerFromDb(guildId, userId); // if unmute errored, just remove unmute from db + _log.Warn("Couldn't unmute user {0} in guild {1}", userId, guildId); + _log.Warn(ex); + } + }, null, after, Timeout.InfiniteTimeSpan); + + //add it, or stop the old one and add this one + userUnmuteTimers.AddOrUpdate(userId, (key) => toAdd, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return toAdd; + }); + } + + public void StopUnmuteTimer(ulong guildId, ulong userId) + { + if (!UnmuteTimers.TryGetValue(guildId, out ConcurrentDictionary userUnmuteTimers)) return; + + if (userUnmuteTimers.TryRemove(userId, out Timer removed)) + { + removed.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + private void RemoveUnmuteTimerFromDb(ulong guildId, ulong userId) + { + using (var uow = _db.UnitOfWork) + { + var config = uow.GuildConfigs.For(guildId, set => set.Include(x => x.UnmuteTimers)); + config.UnmuteTimers.RemoveWhere(x => x.UserId == userId); + uow.Complete(); + } + } + } + + public enum MuteType + { + Voice, + Chat, + All + } +} diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs new file mode 100644 index 00000000..d36afc94 --- /dev/null +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -0,0 +1,48 @@ +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class SelfService + { + public volatile bool ForwardDMs; + public volatile bool ForwardDMsToAllOwners; + + private readonly Logger _log; + private readonly NadekoBot _bot; + private readonly CommandHandler _cmdHandler; + private readonly DbHandler _db; + + public SelfService(NadekoBot bot, CommandHandler cmdHandler, DbHandler db, + BotConfig bc) + { + _bot = bot; + _cmdHandler = cmdHandler; + _db = db; + + using (var uow = _db.UnitOfWork) + { + var config = uow.BotConfig.GetOrCreate(); + ForwardDMs = config.ForwardMessages; + ForwardDMsToAllOwners = config.ForwardToAllOwners; + } + + var _ = Task.Run(async () => + { + while (!bot.Ready) + await Task.Delay(1000); + + foreach (var cmd in bc.StartupCommands) + { + await cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText); + await Task.Delay(400).ConfigureAwait(false); + } + }); + } + } +} diff --git a/src/NadekoBot/Services/Administration/VcRoleService.cs b/src/NadekoBot/Services/Administration/VcRoleService.cs new file mode 100644 index 00000000..057687d9 --- /dev/null +++ b/src/NadekoBot/Services/Administration/VcRoleService.cs @@ -0,0 +1,102 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class VcRoleService + { + private readonly Logger _log; + + public ConcurrentDictionary> VcRoles { get; } + + public VcRoleService(DiscordShardedClient client, IEnumerable gcs) + { + _log = LogManager.GetCurrentClassLogger(); + + client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated; + VcRoles = new ConcurrentDictionary>(); + foreach (var gconf in gcs) + { + var g = client.GetGuild(gconf.GuildId); + if (g == null) + continue; //todo delete everything from db if guild doesn't exist? + + var infos = new ConcurrentDictionary(); + VcRoles.TryAdd(gconf.GuildId, infos); + foreach (var ri in gconf.VcRoleInfos) + { + var role = g.GetRole(ri.RoleId); + if (role == null) + continue; //todo remove this entry from db + + infos.TryAdd(ri.VoiceChannelId, role); + } + } + } + + private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, + SocketVoiceState newState) + { + + var gusr = usr as SocketGuildUser; + if (gusr == null) + return Task.CompletedTask; + + var oldVc = oldState.VoiceChannel; + var newVc = newState.VoiceChannel; + var _ = Task.Run(async () => + { + try + { + if (oldVc != newVc) + { + ulong guildId; + guildId = newVc?.Guild.Id ?? oldVc.Guild.Id; + + if (VcRoles.TryGetValue(guildId, out ConcurrentDictionary guildVcRoles)) + { + //remove old + if (oldVc != null && guildVcRoles.TryGetValue(oldVc.Id, out IRole role)) + { + if (gusr.Roles.Contains(role)) + { + try + { + await gusr.RemoveRoleAsync(role).ConfigureAwait(false); + await Task.Delay(500).ConfigureAwait(false); + } + catch + { + await Task.Delay(200).ConfigureAwait(false); + await gusr.RemoveRoleAsync(role).ConfigureAwait(false); + await Task.Delay(500).ConfigureAwait(false); + } + } + } + //add new + if (newVc != null && guildVcRoles.TryGetValue(newVc.Id, out role)) + { + if (!gusr.Roles.Contains(role)) + await gusr.AddRoleAsync(role).ConfigureAwait(false); + } + + } + } + } + catch (Exception ex) + { + _log.Warn(ex); + } + }); + return Task.CompletedTask; + } + } +} diff --git a/src/NadekoBot/Services/Administration/VplusTService.cs b/src/NadekoBot/Services/Administration/VplusTService.cs new file mode 100644 index 00000000..6d0f8e62 --- /dev/null +++ b/src/NadekoBot/Services/Administration/VplusTService.cs @@ -0,0 +1,157 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class VplusTService + { + private readonly Regex _channelNameRegex = new Regex(@"[^a-zA-Z0-9 -]", RegexOptions.Compiled); + + public readonly ConcurrentHashSet VoicePlusTextCache; + + private readonly ConcurrentDictionary _guildLockObjects = new ConcurrentDictionary(); + private readonly DiscordShardedClient _client; + private readonly NadekoStrings _strings; + private readonly DbHandler _db; + private readonly Logger _log; + + public VplusTService(DiscordShardedClient client, IEnumerable gcs, NadekoStrings strings, + DbHandler db) + { + _client = client; + _strings = strings; + _db = db; + _log = LogManager.GetCurrentClassLogger(); + + VoicePlusTextCache = new ConcurrentHashSet(gcs.Where(g => g.VoicePlusTextEnabled).Select(g => g.GuildId)); + _client.UserVoiceStateUpdated += UserUpdatedEventHandler; + } + + private Task UserUpdatedEventHandler(SocketUser iuser, SocketVoiceState before, SocketVoiceState after) + { + var user = (iuser as SocketGuildUser); + var guild = user?.Guild; + + if (guild == null) + return Task.CompletedTask; + + var botUserPerms = guild.CurrentUser.GuildPermissions; + + if (before.VoiceChannel == after.VoiceChannel) + return Task.CompletedTask; + + if (!VoicePlusTextCache.Contains(guild.Id)) + return Task.CompletedTask; + + var _ = Task.Run(async () => + { + try + { + + if (!botUserPerms.ManageChannels || !botUserPerms.ManageRoles) + { + try + { + await guild.Owner.SendErrorAsync( + _strings.GetText("vt_exit", + guild.Id, + "Administration".ToLowerInvariant(), + Format.Bold(guild.Name))).ConfigureAwait(false); + } + catch + { + // ignored + } + using (var uow = _db.UnitOfWork) + { + uow.GuildConfigs.For(guild.Id, set => set).VoicePlusTextEnabled = false; + VoicePlusTextCache.TryRemove(guild.Id); + await uow.CompleteAsync().ConfigureAwait(false); + } + return; + } + + var semaphore = _guildLockObjects.GetOrAdd(guild.Id, (key) => new SemaphoreSlim(1, 1)); + + try + { + await semaphore.WaitAsync().ConfigureAwait(false); + + var beforeVch = before.VoiceChannel; + if (beforeVch != null) + { + var beforeRoleName = GetRoleName(beforeVch); + var beforeRole = guild.Roles.FirstOrDefault(x => x.Name == beforeRoleName); + if (beforeRole != null) + try + { + _log.Info("Removing role " + beforeRoleName + " from user " + user.Username); + await user.RemoveRoleAsync(beforeRole).ConfigureAwait(false); + await Task.Delay(200).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn(ex); + } + } + var afterVch = after.VoiceChannel; + if (afterVch != null && guild.AFKChannel?.Id != afterVch.Id) + { + var roleName = GetRoleName(afterVch); + var roleToAdd = guild.Roles.FirstOrDefault(x => x.Name == roleName) ?? + (IRole)await guild.CreateRoleAsync(roleName, GuildPermissions.None).ConfigureAwait(false); + + ITextChannel textChannel = guild.TextChannels + .FirstOrDefault(t => t.Name == GetChannelName(afterVch.Name).ToLowerInvariant()); + if (textChannel == null) + { + var created = (await guild.CreateTextChannelAsync(GetChannelName(afterVch.Name).ToLowerInvariant()).ConfigureAwait(false)); + + try { await guild.CurrentUser.AddRoleAsync(roleToAdd).ConfigureAwait(false); } catch {/*ignored*/} + await Task.Delay(50).ConfigureAwait(false); + await created.AddPermissionOverwriteAsync(roleToAdd, new OverwritePermissions( + readMessages: PermValue.Allow, + sendMessages: PermValue.Allow)) + .ConfigureAwait(false); + await Task.Delay(50).ConfigureAwait(false); + await created.AddPermissionOverwriteAsync(guild.EveryoneRole, new OverwritePermissions( + readMessages: PermValue.Deny, + sendMessages: PermValue.Deny)) + .ConfigureAwait(false); + await Task.Delay(50).ConfigureAwait(false); + } + _log.Warn("Adding role " + roleToAdd.Name + " to user " + user.Username); + await user.AddRoleAsync(roleToAdd).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + catch (Exception ex) + { + _log.Warn(ex); + } + }); + return Task.CompletedTask; + } + + public string GetChannelName(string voiceName) => + _channelNameRegex.Replace(voiceName, "").Trim().Replace(" ", "-").TrimTo(90, true) + "-voice"; + + public string GetRoleName(IVoiceChannel ch) => + "nvoice-" + ch.Id; + } +} diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 6c72d118..ed7d52b3 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -544,7 +544,7 @@ namespace NadekoBot.Services //////int price; //////if (Permissions.CommandCostCommands.CommandCosts.TryGetValue(cmd.Aliases.First().Trim().ToLowerInvariant(), out price) && price > 0) //////{ - ////// var success = await CurrencyHandler.RemoveCurrencyAsync(context.User.Id, $"Running {cmd.Name} command.", price).ConfigureAwait(false); + ////// var success = await _ch.RemoveCurrencyAsync(context.User.Id, $"Running {cmd.Name} command.", price).ConfigureAwait(false); ////// if (!success) ////// { ////// return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, $"Insufficient funds. You need {price}{NadekoBot.BotConfig.CurrencySign} to run this command.")); diff --git a/src/NadekoBot/Services/DbHandler.cs b/src/NadekoBot/Services/DbHandler.cs index 2cec74f3..619ccd55 100644 --- a/src/NadekoBot/Services/DbHandler.cs +++ b/src/NadekoBot/Services/DbHandler.cs @@ -19,7 +19,7 @@ namespace NadekoBot.Services var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite(creds.Db.ConnectionString); options = optionsBuilder.Options; - //switch (NadekoBot.Credentials.Db.Type.ToUpperInvariant()) + //switch (_creds.Db.Type.ToUpperInvariant()) //{ // case "SQLITE": // dbType = typeof(NadekoSqliteContext); diff --git a/src/NadekoBot/Services/Games/ChatterBotResponse.cs b/src/NadekoBot/Services/Games/ChatterBotResponse.cs new file mode 100644 index 00000000..b064af4c --- /dev/null +++ b/src/NadekoBot/Services/Games/ChatterBotResponse.cs @@ -0,0 +1,8 @@ +namespace NadekoBot.Services.Games +{ + public class ChatterBotResponse + { + public string Convo_id { get; set; } + public string BotSay { get; set; } + } +} diff --git a/src/NadekoBot/Services/Games/ChatterBotSession.cs b/src/NadekoBot/Services/Games/ChatterBotSession.cs new file mode 100644 index 00000000..7c4f152c --- /dev/null +++ b/src/NadekoBot/Services/Games/ChatterBotSession.cs @@ -0,0 +1,39 @@ +using NadekoBot.Extensions; +using Newtonsoft.Json; +using System.Collections.Immutable; +using System.Net.Http; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Games +{ + public class ChatterBotSession + { + private static NadekoRandom rng { get; } = new NadekoRandom(); + public string ChatterbotId { get; } + public string ChannelId { get; } + private int _botId = 6; + + public ChatterBotSession(ulong channelId) + { + ChannelId = channelId.ToString().ToBase64(); + ChatterbotId = rng.Next(0, 1000000).ToString().ToBase64(); + } + + private string apiEndpoint => "http://api.program-o.com/v2/chatbot/" + + $"?bot_id={_botId}&" + + "say={0}&" + + $"convo_id=nadekobot_{ChatterbotId}_{ChannelId}&" + + "format=json"; + + public async Task Think(string message) + { + using (var http = new HttpClient()) + { + var res = await http.GetStringAsync(string.Format(apiEndpoint, message)).ConfigureAwait(false); + var cbr = JsonConvert.DeserializeObject(res); + //Console.WriteLine(cbr.Convo_id); + return cbr.BotSay.Replace("
", "\n"); + } + } + } +} diff --git a/src/NadekoBot/Services/Games/GamesService.cs b/src/NadekoBot/Services/Games/GamesService.cs new file mode 100644 index 00000000..459c8d32 --- /dev/null +++ b/src/NadekoBot/Services/Games/GamesService.cs @@ -0,0 +1,206 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using Newtonsoft.Json; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Games +{ + public class GamesService + { + private readonly BotConfig _bc; + + public readonly ConcurrentDictionary GirlRatings = new ConcurrentDictionary(); + public readonly ImmutableArray EightBallResponses; + + private readonly Timer _t; + private readonly DiscordShardedClient _client; + private readonly NadekoStrings _strings; + private readonly IImagesService _images; + private readonly Logger _log; + + public ConcurrentDictionary> CleverbotGuilds { get; } + + public readonly string TypingArticlesPath = "data/typing_articles2.json"; + public List TypingArticles { get; } = new List(); + + public GamesService(DiscordShardedClient client, BotConfig bc, IEnumerable gcs, + NadekoStrings strings, IImagesService images) + { + _bc = bc; + _client = client; + _strings = strings; + _images = images; + _log = LogManager.GetCurrentClassLogger(); + + //8ball + EightBallResponses = _bc.EightBallResponses.Select(ebr => ebr.Text).ToImmutableArray(); + + //girl ratings + _t = new Timer((_) => + { + GirlRatings.Clear(); + + }, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1)); + + //cleverbot + CleverbotGuilds = new ConcurrentDictionary>( + gcs.Where(gc => gc.CleverbotEnabled) + .ToDictionary(gc => gc.GuildId, gc => new Lazy(() => new ChatterBotSession(gc.GuildId), true))); + + //plantpick + client.MessageReceived += PotentialFlowerGeneration; + GenerationChannels = new ConcurrentHashSet(gcs + .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); + + try + { + TypingArticles = JsonConvert.DeserializeObject>(File.ReadAllText(TypingArticlesPath)); + } + catch (Exception ex) + { + _log.Warn("Error while loading typing articles {0}", ex.ToString()); + TypingArticles = new List(); + } + } + + + public string PrepareMessage(IUserMessage msg, out ChatterBotSession cleverbot) + { + var channel = msg.Channel as ITextChannel; + cleverbot = null; + + if (channel == null) + return null; + + Lazy lazyCleverbot; + if (!CleverbotGuilds.TryGetValue(channel.Guild.Id, out lazyCleverbot)) + return null; + + cleverbot = lazyCleverbot.Value; + + var nadekoId = _client.CurrentUser.Id; + var normalMention = $"<@{nadekoId}> "; + var nickMention = $"<@!{nadekoId}> "; + string message; + if (msg.Content.StartsWith(normalMention)) + { + message = msg.Content.Substring(normalMention.Length).Trim(); + } + else if (msg.Content.StartsWith(nickMention)) + { + message = msg.Content.Substring(nickMention.Length).Trim(); + } + else + { + return null; + } + + return message; + } + + public async Task TryAsk(ChatterBotSession cleverbot, ITextChannel channel, string message) + { + await channel.TriggerTypingAsync().ConfigureAwait(false); + + var response = await cleverbot.Think(message).ConfigureAwait(false); + try + { + await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false); + } + catch + { + await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false); // try twice :\ + } + return true; + } + + public ConcurrentHashSet GenerationChannels { get; } + //channelid/message + public ConcurrentDictionary> PlantedFlowers { get; } = new ConcurrentDictionary>(); + //channelId/last generation + public ConcurrentDictionary LastGenerations { get; } = new ConcurrentDictionary(); + + public KeyValuePair> GetRandomCurrencyImage() + { + var rng = new NadekoRandom(); + return _images.Currency[rng.Next(0, _images.Currency.Length)]; + } + + private string GetText(ITextChannel ch, string key, params object[] rep) + => _strings.GetText(key, ch.GuildId, "Games".ToLowerInvariant(), rep); + + private Task PotentialFlowerGeneration(SocketMessage imsg) + { + var msg = imsg as SocketUserMessage; + if (msg == null || msg.Author.IsBot) + return Task.CompletedTask; + + var channel = imsg.Channel as ITextChannel; + if (channel == null) + return Task.CompletedTask; + + if (!GenerationChannels.Contains(channel.Id)) + return Task.CompletedTask; + + var _ = Task.Run(async () => + { + try + { + var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue); + var rng = new NadekoRandom(); + + //todo i'm stupid :rofl: wtg kwoth. real async programming :100: :ok_hand: :100: :100: :thumbsup: + if (DateTime.Now - TimeSpan.FromSeconds(_bc.CurrencyGenerationCooldown) < lastGeneration) //recently generated in this channel, don't generate again + return; + + var num = rng.Next(1, 101) + _bc.CurrencyGenerationChance * 100; + + if (num > 100) + { + LastGenerations.AddOrUpdate(channel.Id, DateTime.Now, (id, old) => DateTime.Now); + + var dropAmount = _bc.CurrencyDropAmount; + + if (dropAmount > 0) + { + var msgs = new IUserMessage[dropAmount]; + var prefix = NadekoBot.Prefix; + var toSend = dropAmount == 1 + ? GetText(channel, "curgen_sn", _bc.CurrencySign) + + " " + GetText(channel, "pick_sn", prefix) + : GetText(channel, "curgen_pl", dropAmount, _bc.CurrencySign) + + " " + GetText(channel, "pick_pl", prefix); + var file = GetRandomCurrencyImage(); + using (var fileStream = file.Value.ToStream()) + { + var sent = await channel.SendFileAsync( + fileStream, + file.Key, + toSend).ConfigureAwait(false); + + msgs[0] = sent; + } + + PlantedFlowers.AddOrUpdate(channel.Id, msgs.ToList(), (id, old) => { old.AddRange(msgs); return old; }); + } + } + } + catch (Exception ex) + { + LogManager.GetCurrentClassLogger().Warn(ex); + } + }); + return Task.CompletedTask; + } + } +} diff --git a/src/NadekoBot/Services/Games/GirlRating.cs b/src/NadekoBot/Services/Games/GirlRating.cs new file mode 100644 index 00000000..9cf72945 --- /dev/null +++ b/src/NadekoBot/Services/Games/GirlRating.cs @@ -0,0 +1,71 @@ +using ImageSharp; +using NadekoBot.DataStructures; +using NadekoBot.Extensions; +using NLog; +using System; +using System.IO; +using System.Linq; +using System.Net.Http; + +namespace NadekoBot.Services.Games +{ + public class GirlRating + { + private static readonly Logger _log = LogManager.GetCurrentClassLogger(); + + public double Crazy { get; } + public double Hot { get; } + public int Roll { get; } + public string Advice { get; } + public AsyncLazy Url { get; } + + public GirlRating(IImagesService _images, double crazy, double hot, int roll, string advice) + { + Crazy = crazy; + Hot = hot; + Roll = roll; + Advice = advice; // convenient to have it here, even though atm there are only few different ones. + + Url = new AsyncLazy(async () => + { + try + { + using (var ms = new MemoryStream(_images.WifeMatrix.ToArray(), false)) + using (var img = new ImageSharp.Image(ms)) + { + const int minx = 35; + const int miny = 385; + const int length = 345; + + var pointx = (int)(minx + length * (Hot / 10)); + var pointy = (int)(miny - length * ((Crazy - 4) / 6)); + + using (var pointMs = new MemoryStream(_images.RategirlDot.ToArray(), false)) + using (var pointImg = new ImageSharp.Image(pointMs)) + { + img.DrawImage(pointImg, 100, default(Size), new Point(pointx - 10, pointy - 10)); + } + + string url; + using (var http = new HttpClient()) + using (var imgStream = new MemoryStream()) + { + img.Save(imgStream); + var byteContent = new ByteArrayContent(imgStream.ToArray()); + http.AddFakeHeaders(); + + var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent); + url = await reponse.Content.ReadAsStringAsync(); + } + return url; + } + } + catch (Exception ex) + { + _log.Warn(ex); + return null; + } + }); + } + } +} diff --git a/src/NadekoBot/Modules/Games/Commands/Models/TypingArticle.cs b/src/NadekoBot/Services/Games/TypingArticle.cs similarity index 71% rename from src/NadekoBot/Modules/Games/Commands/Models/TypingArticle.cs rename to src/NadekoBot/Services/Games/TypingArticle.cs index e5697772..2024704f 100644 --- a/src/NadekoBot/Modules/Games/Commands/Models/TypingArticle.cs +++ b/src/NadekoBot/Services/Games/TypingArticle.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Games.Commands.Models +namespace NadekoBot.Services.Games { public class TypingArticle { diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index ba866433..1e427e65 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -1,6 +1,10 @@ -using NadekoBot.Extensions; +using Discord; +using Discord.WebSocket; +using NadekoBot.DataStructures; +using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; +using NLog; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -15,11 +19,158 @@ namespace NadekoBot.Services private readonly DbHandler _db; public readonly ConcurrentDictionary GuildConfigsCache; + private readonly DiscordShardedClient _client; + private readonly Logger _log; - public GreetSettingsService(IEnumerable guildConfigs, DbHandler db) + public GreetSettingsService(DiscordShardedClient client, IEnumerable guildConfigs, DbHandler db) { _db = db; + _client = client; + _log = LogManager.GetCurrentClassLogger(); + GuildConfigsCache = new ConcurrentDictionary(guildConfigs.ToDictionary(g => g.GuildId, GreetSettings.Create)); + + _client.UserJoined += UserJoined; + _client.UserLeft += UserLeft; + } + + private Task UserLeft(IGuildUser user) + { + var _ = Task.Run(async () => + { + try + { + var conf = GetOrAddSettingsForGuild(user.GuildId); + + if (!conf.SendChannelByeMessage) return; + var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.ByeMessageChannelId); + + if (channel == null) //maybe warn the server owner that the channel is missing + return; + CREmbed embedData; + if (CREmbed.TryParse(conf.ChannelByeMessageText, out embedData)) + { + embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + embedData.Description = embedData.Description?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + embedData.Title = embedData.Title?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + try + { + var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + if (conf.AutoDeleteByeMessagesTimer > 0) + { + toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); + } + } + catch (Exception ex) { _log.Warn(ex); } + } + else + { + var msg = conf.ChannelByeMessageText.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + if (string.IsNullOrWhiteSpace(msg)) + return; + try + { + var toDelete = await channel.SendMessageAsync(msg.SanitizeMentions()).ConfigureAwait(false); + if (conf.AutoDeleteByeMessagesTimer > 0) + { + toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); + } + } + catch (Exception ex) { _log.Warn(ex); } + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private Task UserJoined(IGuildUser user) + { + var _ = Task.Run(async () => + { + try + { + var conf = GetOrAddSettingsForGuild(user.GuildId); + + if (conf.SendChannelGreetMessage) + { + var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.GreetMessageChannelId); + if (channel != null) //maybe warn the server owner that the channel is missing + { + + CREmbed embedData; + if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData)) + { + embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + embedData.Description = embedData.Description?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + try + { + var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + if (conf.AutoDeleteGreetMessagesTimer > 0) + { + toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); + } + } + catch (Exception ex) { _log.Warn(ex); } + } + else + { + var msg = conf.ChannelGreetMessageText.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + if (!string.IsNullOrWhiteSpace(msg)) + { + try + { + var toDelete = await channel.SendMessageAsync(msg.SanitizeMentions()).ConfigureAwait(false); + if (conf.AutoDeleteGreetMessagesTimer > 0) + { + toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); + } + } + catch (Exception ex) { _log.Warn(ex); } + } + } + } + } + + if (conf.SendDmGreetMessage) + { + var channel = await user.CreateDMChannelAsync(); + + if (channel != null) + { + CREmbed embedData; + if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData)) + { + embedData.PlainText = embedData.PlainText?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + embedData.Description = embedData.Description?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + try + { + await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex); } + } + else + { + var msg = conf.DmGreetMessageText.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + if (!string.IsNullOrWhiteSpace(msg)) + { + await channel.SendConfirmAsync(msg).ConfigureAwait(false); + } + } + } + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; } public GreetSettings GetOrAddSettingsForGuild(ulong guildId) @@ -210,6 +361,23 @@ namespace NadekoBot.Services await uow.CompleteAsync().ConfigureAwait(false); } } + + public async Task SetGreetDel(ulong id, int timer) + { + if (timer < 0 || timer > 600) + return; + + using (var uow = _db.UnitOfWork) + { + var conf = uow.GuildConfigs.For(id, set => set); + conf.AutoDeleteGreetMessagesTimer = timer; + + var toAdd = GreetSettings.Create(conf); + GuildConfigsCache.AddOrUpdate(id, toAdd, (key, old) => toAdd); + + await uow.CompleteAsync().ConfigureAwait(false); + } + } } public class GreetSettings diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index 82299319..ccc75d27 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -17,7 +17,8 @@ namespace NadekoBot.Services string CarbonKey { get; } DBConfig Db { get; } - string SoundCloudClientId { get; set; } + string SoundCloudClientId { get; } + string OsuApiKey { get; } bool IsOwner(IUser u); } diff --git a/src/NadekoBot/Services/IGoogleApiService.cs b/src/NadekoBot/Services/IGoogleApiService.cs index b79bdb64..2ecf1405 100644 --- a/src/NadekoBot/Services/IGoogleApiService.cs +++ b/src/NadekoBot/Services/IGoogleApiService.cs @@ -1,4 +1,5 @@ -using System; +using Google.Apis.Customsearch.v1.Data; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -6,12 +7,28 @@ namespace NadekoBot.Services { public interface IGoogleApiService { + IEnumerable Languages { get; } + Task> GetVideosByKeywordsAsync(string keywords, int count = 1); Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1); Task> GetRelatedVideosAsync(string url, int count = 1); Task> GetPlaylistTracksAsync(string playlistId, int count = 50); Task> GetVideoDurationsAsync(IEnumerable videoIds); + Task GetImageAsync(string query, int start = 1); + Task Translate(string sourceText, string sourceLanguage, string targetLanguage); Task ShortenUrl(string url); } + + public struct ImageResult + { + public Result.ImageData Image { get; } + public string Link { get; } + + public ImageResult(Result.ImageData image, string link) + { + this.Image = image; + this.Link = link; + } + } } diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 27ea4090..82338207 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -32,7 +32,7 @@ namespace NadekoBot.Services.Impl ? "d0bd7768e3a1a2d15430f0dccb871117" : _soundcloudClientId; } - set { + private set { _soundcloudClientId = value; } } diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index c4afec9f..6be8089c 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -9,7 +9,9 @@ using Google.Apis.Urlshortener.v1; using Google.Apis.Urlshortener.v1.Data; using NLog; using Google.Apis.Customsearch.v1; -using Google.Apis.Customsearch.v1.Data; +using System.Net.Http; +using System.Net; +using Newtonsoft.Json.Linq; namespace NadekoBot.Services.Impl { @@ -20,7 +22,7 @@ namespace NadekoBot.Services.Impl private YouTubeService yt; private UrlshortenerService sh; private CustomsearchService cs; - + private Logger _log { get; } public GoogleApiService(IBotCredentials creds) @@ -39,6 +41,7 @@ namespace NadekoBot.Services.Impl sh = new UrlshortenerService(bcs); cs = new CustomsearchService(bcs); } + public async Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1) { if (string.IsNullOrWhiteSpace(keywords)) @@ -190,18 +193,6 @@ namespace NadekoBot.Services.Impl return toReturn; } - public struct ImageResult - { - public Result.ImageData Image { get; } - public string Link { get; } - - public ImageResult(Result.ImageData image, string link) - { - this.Image = image; - this.Link = link; - } - } - public async Task GetImageAsync(string query, int start = 1) { if (string.IsNullOrWhiteSpace(query)) @@ -218,5 +209,168 @@ namespace NadekoBot.Services.Impl return new ImageResult(search.Items[0].Image, search.Items[0].Link); } + + public IEnumerable Languages => _languageDictionary.Keys.OrderBy(x => x); + private readonly Dictionary _languageDictionary = new Dictionary() { + { "afrikaans", "af"}, + { "albanian", "sq"}, + { "arabic", "ar"}, + { "armenian", "hy"}, + { "azerbaijani", "az"}, + { "basque", "eu"}, + { "belarusian", "be"}, + { "bengali", "bn"}, + { "bulgarian", "bg"}, + { "catalan", "ca"}, + { "chinese-traditional", "zh-TW"}, + { "chinese-simplified", "zh-CN"}, + { "chinese", "zh-CN"}, + { "croatian", "hr"}, + { "czech", "cs"}, + { "danish", "da"}, + { "dutch", "nl"}, + { "english", "en"}, + { "esperanto", "eo"}, + { "estonian", "et"}, + { "filipino", "tl"}, + { "finnish", "fi"}, + { "french", "fr"}, + { "galician", "gl"}, + { "german", "de"}, + { "georgian", "ka"}, + { "greek", "el"}, + { "haitian Creole", "ht"}, + { "hebrew", "iw"}, + { "hindi", "hi"}, + { "hungarian", "hu"}, + { "icelandic", "is"}, + { "indonesian", "id"}, + { "irish", "ga"}, + { "italian", "it"}, + { "japanese", "ja"}, + { "korean", "ko"}, + { "lao", "lo"}, + { "latin", "la"}, + { "latvian", "lv"}, + { "lithuanian", "lt"}, + { "macedonian", "mk"}, + { "malay", "ms"}, + { "maltese", "mt"}, + { "norwegian", "no"}, + { "persian", "fa"}, + { "polish", "pl"}, + { "portuguese", "pt"}, + { "romanian", "ro"}, + { "russian", "ru"}, + { "serbian", "sr"}, + { "slovak", "sk"}, + { "slovenian", "sl"}, + { "spanish", "es"}, + { "swahili", "sw"}, + { "swedish", "sv"}, + { "tamil", "ta"}, + { "telugu", "te"}, + { "thai", "th"}, + { "turkish", "tr"}, + { "ukrainian", "uk"}, + { "urdu", "ur"}, + { "vietnamese", "vi"}, + { "welsh", "cy"}, + { "yiddish", "yi"}, + + { "af", "af"}, + { "sq", "sq"}, + { "ar", "ar"}, + { "hy", "hy"}, + { "az", "az"}, + { "eu", "eu"}, + { "be", "be"}, + { "bn", "bn"}, + { "bg", "bg"}, + { "ca", "ca"}, + { "zh-tw", "zh-TW"}, + { "zh-cn", "zh-CN"}, + { "hr", "hr"}, + { "cs", "cs"}, + { "da", "da"}, + { "nl", "nl"}, + { "en", "en"}, + { "eo", "eo"}, + { "et", "et"}, + { "tl", "tl"}, + { "fi", "fi"}, + { "fr", "fr"}, + { "gl", "gl"}, + { "de", "de"}, + { "ka", "ka"}, + { "el", "el"}, + { "ht", "ht"}, + { "iw", "iw"}, + { "hi", "hi"}, + { "hu", "hu"}, + { "is", "is"}, + { "id", "id"}, + { "ga", "ga"}, + { "it", "it"}, + { "ja", "ja"}, + { "ko", "ko"}, + { "lo", "lo"}, + { "la", "la"}, + { "lv", "lv"}, + { "lt", "lt"}, + { "mk", "mk"}, + { "ms", "ms"}, + { "mt", "mt"}, + { "no", "no"}, + { "fa", "fa"}, + { "pl", "pl"}, + { "pt", "pt"}, + { "ro", "ro"}, + { "ru", "ru"}, + { "sr", "sr"}, + { "sk", "sk"}, + { "sl", "sl"}, + { "es", "es"}, + { "sw", "sw"}, + { "sv", "sv"}, + { "ta", "ta"}, + { "te", "te"}, + { "th", "th"}, + { "tr", "tr"}, + { "uk", "uk"}, + { "ur", "ur"}, + { "vi", "vi"}, + { "cy", "cy"}, + { "yi", "yi"}, + }; + + public async Task Translate(string sourceText, string sourceLanguage, string targetLanguage) + { + string text; + + if (!_languageDictionary.ContainsKey(sourceLanguage) || + !_languageDictionary.ContainsKey(targetLanguage)) + throw new ArgumentException(); + + + var url = string.Format("https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}", + ConvertToLanguageCode(sourceLanguage), + ConvertToLanguageCode(targetLanguage), + WebUtility.UrlEncode(sourceText)); + using (var http = new HttpClient()) + { + http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); + text = await http.GetStringAsync(url).ConfigureAwait(false); + } + + return (string.Concat(JArray.Parse(text)[0].Select(x => x[0]))); + } + + private string ConvertToLanguageCode(string language) + { + string mode; + _languageDictionary.TryGetValue(language, out mode); + return mode; + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/NadekoStrings.cs b/src/NadekoBot/Services/Impl/NadekoStrings.cs index 9eaede58..deff344a 100644 --- a/src/NadekoBot/Services/Impl/NadekoStrings.cs +++ b/src/NadekoBot/Services/Impl/NadekoStrings.cs @@ -7,6 +7,7 @@ using NLog; using System.Diagnostics; using Newtonsoft.Json; using System; +using Discord; namespace NadekoBot.Services { @@ -20,10 +21,13 @@ namespace NadekoBot.Services /// Used as failsafe in case response key doesn't exist in the selected or default language. /// private readonly CultureInfo _usCultureInfo = new CultureInfo("en-US"); + private readonly ILocalization _localization; - public NadekoStrings() + public NadekoStrings(ILocalization loc) { _log = LogManager.GetCurrentClassLogger(); + _localization = loc; + var sw = Stopwatch.StartNew(); var allLangsDict = new Dictionary>(); // lang:(name:value) foreach (var file in Directory.GetFiles(stringsPath)) @@ -58,6 +62,9 @@ namespace NadekoBot.Services return val; } + public string GetText(string key, ulong guildId, string lowerModuleTypeName, params object[] replacements) => + GetText(key, _localization.GetCultureInfo(guildId), lowerModuleTypeName); + public string GetText(string key, CultureInfo cultureInfo, string lowerModuleTypeName) { var text = GetString(lowerModuleTypeName + "_" + key, cultureInfo); diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/MagicItem.cs b/src/NadekoBot/Services/Searches/MagicItem.cs similarity index 60% rename from src/NadekoBot/Modules/Searches/Commands/Models/MagicItem.cs rename to src/NadekoBot/Services/Searches/MagicItem.cs index 8c026894..1d555e7e 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/MagicItem.cs +++ b/src/NadekoBot/Services/Searches/MagicItem.cs @@ -1,6 +1,6 @@ -namespace NadekoBot.Modules.Searches.Models +namespace NadekoBot.Services.Searches { - class MagicItem + public class MagicItem { public string Name { get; set; } public string Description { get; set; } diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/SearchPokemon.cs b/src/NadekoBot/Services/Searches/SearchPokemon.cs similarity index 97% rename from src/NadekoBot/Modules/Searches/Commands/Models/SearchPokemon.cs rename to src/NadekoBot/Services/Searches/SearchPokemon.cs index ed758f7f..4fd0674d 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/SearchPokemon.cs +++ b/src/NadekoBot/Services/Searches/SearchPokemon.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NadekoBot.Modules.Searches.Models +namespace NadekoBot.Services.Searches { public class SearchPokemon { diff --git a/src/NadekoBot/Services/Searches/SearchesService.cs b/src/NadekoBot/Services/Searches/SearchesService.cs index a65d5120..53b9cabb 100644 --- a/src/NadekoBot/Services/Searches/SearchesService.cs +++ b/src/NadekoBot/Services/Searches/SearchesService.cs @@ -1,4 +1,12 @@ -using NadekoBot.Extensions; +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using Newtonsoft.Json; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; using System.Net.Http; using System.Threading.Tasks; using System.Xml; @@ -7,6 +15,101 @@ namespace NadekoBot.Services.Searches { public class SearchesService { + private readonly DiscordShardedClient _client; + private readonly IGoogleApiService _google; + private readonly DbHandler _db; + private readonly Logger _log; + + public ConcurrentDictionary TranslatedChannels { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary UserLanguages { get; } = new ConcurrentDictionary(); + + public readonly string PokemonAbilitiesFile = "data/pokemon/pokemon_abilities7.json"; + public readonly string PokemonListFile = "data/pokemon/pokemon_list7.json"; + public Dictionary Pokemons { get; } = new Dictionary(); + public Dictionary PokemonAbilities { get; } = new Dictionary(); + + public List WowJokes { get; } = new List(); + public List MagicItems { get; } = new List(); + + public SearchesService(DiscordShardedClient client, IGoogleApiService google, DbHandler db) + { + _client = client; + _google = google; + _db = db; + _log = LogManager.GetCurrentClassLogger(); + + //translate commands + _client.MessageReceived += async (msg) => + { + try + { + var umsg = msg as SocketUserMessage; + if (umsg == null) + return; + + if (!TranslatedChannels.TryGetValue(umsg.Channel.Id, out var autoDelete)) + return; + + var key = new UserChannelPair() + { + UserId = umsg.Author.Id, + ChannelId = umsg.Channel.Id, + }; + + string langs; + if (!UserLanguages.TryGetValue(key, out langs)) + return; + + var text = await Translate(langs, umsg.Resolve(TagHandling.Ignore)) + .ConfigureAwait(false); + if (autoDelete) + try { await umsg.DeleteAsync().ConfigureAwait(false); } catch { } + await umsg.Channel.SendConfirmAsync($"{umsg.Author.Mention} `:` " + text.Replace("<@ ", "<@").Replace("<@! ", "<@!")).ConfigureAwait(false); + } + catch { } + }; + + //pokemon commands + if (File.Exists(PokemonListFile)) + { + Pokemons = JsonConvert.DeserializeObject>(File.ReadAllText(PokemonListFile)); + } + else + _log.Warn(PokemonListFile + " is missing. Pokemon abilities not loaded."); + if (File.Exists(PokemonAbilitiesFile)) + PokemonAbilities = JsonConvert.DeserializeObject>(File.ReadAllText(PokemonAbilitiesFile)); + else + _log.Warn(PokemonAbilitiesFile + " is missing. Pokemon abilities not loaded."); + + //joke commands + if (File.Exists("data/wowjokes.json")) + { + WowJokes = JsonConvert.DeserializeObject>(File.ReadAllText("data/wowjokes.json")); + } + else + _log.Warn("data/wowjokes.json is missing. WOW Jokes are not loaded."); + + if (File.Exists("data/magicitems.json")) + { + MagicItems = JsonConvert.DeserializeObject>(File.ReadAllText("data/magicitems.json")); + } + else + _log.Warn("data/magicitems.json is missing. Magic items are not loaded."); + } + + public async Task Translate(string langs, string text = null) + { + var langarr = langs.ToLowerInvariant().Split('>'); + if (langarr.Length != 2) + throw new ArgumentException(); + var from = langarr[0]; + var to = langarr[1]; + text = text?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + throw new ArgumentException(); + return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(); + } + public async Task DapiSearch(string tag, DapiSearchType type) { tag = tag?.Replace(" ", "_"); @@ -65,4 +168,18 @@ namespace NadekoBot.Services.Searches Rule34, Yandere } + + public struct UserChannelPair + { + public ulong UserId { get; set; } + public ulong ChannelId { get; set; } + } + + + public class StreamStatus + { + public bool IsLive { get; set; } + public string ApiLink { get; set; } + public string Views { get; set; } + } } diff --git a/src/NadekoBot/Services/Searches/StreamNotFoundException.cs b/src/NadekoBot/Services/Searches/StreamNotFoundException.cs new file mode 100644 index 00000000..f05d250f --- /dev/null +++ b/src/NadekoBot/Services/Searches/StreamNotFoundException.cs @@ -0,0 +1,11 @@ +using System; + +namespace NadekoBot.Services.Searches +{ + public class StreamNotFoundException : Exception + { + public StreamNotFoundException(string message) : base($"Stream '{message}' not found.") + { + } + } +} diff --git a/src/NadekoBot/Services/Searches/StreamNotificationService.cs b/src/NadekoBot/Services/Searches/StreamNotificationService.cs new file mode 100644 index 00000000..1fcaacb5 --- /dev/null +++ b/src/NadekoBot/Services/Searches/StreamNotificationService.cs @@ -0,0 +1,190 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Searches +{ + public class StreamNotificationService + { + private readonly Timer _streamCheckTimer; + private bool firstStreamNotifPass { get; set; } = true; + private readonly ConcurrentDictionary _cachedStatuses = new ConcurrentDictionary(); + + private readonly DbHandler _db; + private readonly DiscordShardedClient _client; + private readonly NadekoStrings _strings; + + public StreamNotificationService(DbHandler db, DiscordShardedClient client, NadekoStrings strings) + { + _db = db; + _client = client; + _strings = strings; + } + + public StreamNotificationService() + { + _streamCheckTimer = new Timer(async (state) => + { + var oldCachedStatuses = new ConcurrentDictionary(_cachedStatuses); + _cachedStatuses.Clear(); + IEnumerable streams; + using (var uow = _db.UnitOfWork) + { + streams = uow.GuildConfigs.GetAllFollowedStreams(); + } + + await Task.WhenAll(streams.Select(async fs => + { + try + { + var newStatus = await GetStreamStatus(fs).ConfigureAwait(false); + if (firstStreamNotifPass) + { + return; + } + + StreamStatus oldStatus; + if (oldCachedStatuses.TryGetValue(newStatus.ApiLink, out oldStatus) && + oldStatus.IsLive != newStatus.IsLive) + { + var server = _client.GetGuild(fs.GuildId); + var channel = server?.GetTextChannel(fs.ChannelId); + if (channel == null) + return; + try + { + await channel.EmbedAsync(GetEmbed(fs, newStatus, channel.Guild.Id)).ConfigureAwait(false); + } + catch + { + // ignored + } + } + } + catch + { + // ignored + } + })); + + firstStreamNotifPass = false; + }, null, TimeSpan.Zero, TimeSpan.FromSeconds(60)); + } + + public async Task GetStreamStatus(FollowedStream stream, bool checkCache = true) + { + string response; + StreamStatus result; + switch (stream.Type) + { + case FollowedStream.FollowedStreamType.Hitbox: + var hitboxUrl = $"https://api.hitbox.tv/media/status/{stream.Username.ToLowerInvariant()}"; + if (checkCache && _cachedStatuses.TryGetValue(hitboxUrl, out result)) + return result; + using (var http = new HttpClient()) + { + response = await http.GetStringAsync(hitboxUrl).ConfigureAwait(false); + } + var hbData = JsonConvert.DeserializeObject(response); + if (!hbData.Success) + throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]"); + result = new StreamStatus() + { + IsLive = hbData.IsLive, + ApiLink = hitboxUrl, + Views = hbData.Views + }; + _cachedStatuses.AddOrUpdate(hitboxUrl, result, (key, old) => result); + return result; + case FollowedStream.FollowedStreamType.Twitch: + var twitchUrl = $"https://api.twitch.tv/kraken/streams/{Uri.EscapeUriString(stream.Username.ToLowerInvariant())}?client_id=67w6z9i09xv2uoojdm9l0wsyph4hxo6"; + if (checkCache && _cachedStatuses.TryGetValue(twitchUrl, out result)) + return result; + using (var http = new HttpClient()) + { + response = await http.GetStringAsync(twitchUrl).ConfigureAwait(false); + } + var twData = JsonConvert.DeserializeObject(response); + if (twData.Error != null) + { + throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]"); + } + result = new StreamStatus() + { + IsLive = twData.IsLive, + ApiLink = twitchUrl, + Views = twData.Stream?.Viewers.ToString() ?? "0" + }; + _cachedStatuses.AddOrUpdate(twitchUrl, result, (key, old) => result); + return result; + case FollowedStream.FollowedStreamType.Beam: + var beamUrl = $"https://beam.pro/api/v1/channels/{stream.Username.ToLowerInvariant()}"; + if (checkCache && _cachedStatuses.TryGetValue(beamUrl, out result)) + return result; + using (var http = new HttpClient()) + { + response = await http.GetStringAsync(beamUrl).ConfigureAwait(false); + } + + var bmData = JsonConvert.DeserializeObject(response); + if (bmData.Error != null) + throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]"); + result = new StreamStatus() + { + IsLive = bmData.IsLive, + ApiLink = beamUrl, + Views = bmData.ViewersCurrent.ToString() + }; + _cachedStatuses.AddOrUpdate(beamUrl, result, (key, old) => result); + return result; + default: + break; + } + return null; + } + + public EmbedBuilder GetEmbed(FollowedStream fs, StreamStatus status, ulong guildId) + { + var embed = new EmbedBuilder().WithTitle(fs.Username) + .WithUrl(GetLink(fs)) + .AddField(efb => efb.WithName(GetText(fs, "status")) + .WithValue(status.IsLive ? "Online" : "Offline") + .WithIsInline(true)) + .AddField(efb => efb.WithName(GetText(fs, "viewers")) + .WithValue(status.IsLive ? status.Views : "-") + .WithIsInline(true)) + .AddField(efb => efb.WithName(GetText(fs, "platform")) + .WithValue(fs.Type.ToString()) + .WithIsInline(true)) + .WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor); + + return embed; + } + + public string GetText(FollowedStream fs, string key, params object[] replacements) => + _strings.GetText(key, + fs.GuildId, + "Searches".ToLowerInvariant(), + replacements); + + public string GetLink(FollowedStream fs) + { + if (fs.Type == FollowedStream.FollowedStreamType.Hitbox) + return $"http://www.hitbox.tv/{fs.Username}/"; + if (fs.Type == FollowedStream.FollowedStreamType.Twitch) + return $"http://www.twitch.tv/{fs.Username}/"; + if (fs.Type == FollowedStream.FollowedStreamType.Beam) + return $"https://beam.pro/{fs.Username}/"; + return "??"; + } + } +} diff --git a/src/NadekoBot/Services/Searches/StreamResponses.cs b/src/NadekoBot/Services/Searches/StreamResponses.cs new file mode 100644 index 00000000..484a4962 --- /dev/null +++ b/src/NadekoBot/Services/Searches/StreamResponses.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; + +namespace NadekoBot.Services.Searches +{ + public class HitboxResponse + { + public bool Success { get; set; } = true; + [JsonProperty("media_is_live")] + public string MediaIsLive { get; set; } + public bool IsLive => MediaIsLive == "1"; + [JsonProperty("media_views")] + public string Views { get; set; } + } + + public class TwitchResponse + { + public string Error { get; set; } = null; + public bool IsLive => Stream != null; + public StreamInfo Stream { get; set; } + + public class StreamInfo + { + public int Viewers { get; set; } + } + } + + public class BeamResponse + { + public string Error { get; set; } = null; + + [JsonProperty("online")] + public bool IsLive { get; set; } + public int ViewersCurrent { get; set; } + } +} diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/WoWJoke.cs b/src/NadekoBot/Services/Searches/WoWJoke.cs similarity index 81% rename from src/NadekoBot/Modules/Searches/Commands/Models/WoWJoke.cs rename to src/NadekoBot/Services/Searches/WoWJoke.cs index b353abd9..d363b8b2 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/WoWJoke.cs +++ b/src/NadekoBot/Services/Searches/WoWJoke.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Searches.Models +namespace NadekoBot.Services.Searches { public class WoWJoke {