Restructured folders and project names, ci should be fixed

This commit is contained in:
Kwoth
2021-06-17 23:40:48 +02:00
parent 7aca29ae8a
commit 91ecf9ca41
788 changed files with 204 additions and 146 deletions

View File

@@ -0,0 +1,185 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Common.Replacements;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Administration.Services
{
public class AdministrationService : INService
{
public ConcurrentHashSet<ulong> DeleteMessagesOnCommand { get; }
public ConcurrentDictionary<ulong, bool> DeleteMessagesOnCommandChannels { get; }
private readonly DbService _db;
private readonly LogCommandService _logService;
public AdministrationService(NadekoBot bot, CommandHandler cmdHandler, DbService db,
LogCommandService logService)
{
_db = db;
_logService = logService;
DeleteMessagesOnCommand = new ConcurrentHashSet<ulong>(bot.AllGuildConfigs
.Where(g => g.DeleteMessageOnCommand)
.Select(g => g.GuildId));
DeleteMessagesOnCommandChannels = new ConcurrentDictionary<ulong, bool>(bot.AllGuildConfigs
.SelectMany(x => x.DelMsgOnCmdChannels)
.ToDictionary(x => x.ChannelId, x => x.State)
.ToConcurrent());
cmdHandler.CommandExecuted += DelMsgOnCmd_Handler;
}
public (bool DelMsgOnCmd, IEnumerable<DelMsgOnCmdChannel> channels) GetDelMsgOnCmdData(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
var conf = uow.GuildConfigs.ForId(guildId,
set => set.Include(x => x.DelMsgOnCmdChannels));
return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels);
}
}
private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd)
{
var _ = Task.Run(async () =>
{
if (!(msg.Channel is SocketTextChannel channel))
return;
//wat ?!
if (DeleteMessagesOnCommandChannels.TryGetValue(channel.Id, out var state))
{
if (state && cmd.Name != "prune" && cmd.Name != "pick")
{
_logService.AddDeleteIgnore(msg.Id);
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
}
//if state is false, that means do not do it
}
else if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick")
{
_logService.AddDeleteIgnore(msg.Id);
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
}
});
return Task.CompletedTask;
}
public bool ToggleDeleteMessageOnCommand(ulong guildId)
{
bool enabled;
using (var uow = _db.GetDbContext())
{
var conf = uow.GuildConfigs.ForId(guildId, set => set);
enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand;
uow.SaveChanges();
}
return enabled;
}
public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState)
{
using (var uow = _db.GetDbContext())
{
var conf = uow.GuildConfigs.ForId(guildId,
set => set.Include(x => x.DelMsgOnCmdChannels));
var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId);
if (newState == Administration.State.Inherit)
{
if (!(old is null))
{
conf.DelMsgOnCmdChannels.Remove(old);
uow._context.Remove(old);
}
}
else
{
if (old is null)
{
old = new DelMsgOnCmdChannel { ChannelId = chId };
conf.DelMsgOnCmdChannels.Add(old);
}
old.State = newState == Administration.State.Enable;
DeleteMessagesOnCommandChannels[chId] = newState == Administration.State.Enable;
}
await uow.SaveChangesAsync();
}
if (newState == Administration.State.Disable)
{
}
else if (newState == Administration.State.Enable)
{
DeleteMessagesOnCommandChannels[chId] = true;
}
else
{
DeleteMessagesOnCommandChannels.TryRemove(chId, out var _);
}
}
public async Task DeafenUsers(bool value, params IGuildUser[] users)
{
if (!users.Any())
return;
foreach (var u in users)
{
try
{
await u.ModifyAsync(usr => usr.Deaf = value).ConfigureAwait(false);
}
catch
{
// ignored
}
}
}
public async Task EditMessage(ICommandContext context, ITextChannel chanl, ulong messageId, string text)
{
var msg = await chanl.GetMessageAsync(messageId);
if (!(msg is IUserMessage umsg) || msg.Author.Id != context.Client.CurrentUser.Id)
return;
var rep = new ReplacementBuilder()
.WithDefault(context)
.Build();
if (CREmbed.TryParse(text, out var crembed))
{
rep.Replace(crembed);
await umsg.ModifyAsync(x =>
{
x.Embed = crembed.ToEmbed().Build();
x.Content = crembed.PlainText?.SanitizeMentions() ?? "";
}).ConfigureAwait(false);
}
else
{
await umsg.ModifyAsync(x =>
{
x.Content = text.SanitizeMentions();
x.Embed = null;
}).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Discord.WebSocket;
using NadekoBot.Core.Services;
using System.Collections.Generic;
using System.Threading.Channels;
using LinqToDB;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public sealed class AutoAssignRoleService : INService
{
private readonly DiscordSocketClient _client;
private readonly DbService _db;
//guildid/roleid
private readonly ConcurrentDictionary<ulong, IReadOnlyList<ulong>> _autoAssignableRoles;
private Channel<SocketGuildUser> _assignQueue = Channel.CreateBounded<SocketGuildUser>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false,
});
public AutoAssignRoleService(DiscordSocketClient client, NadekoBot bot, DbService db)
{
_client = client;
_db = db;
_autoAssignableRoles = bot.AllGuildConfigs
.Where(x => !string.IsNullOrWhiteSpace(x.AutoAssignRoleIds))
.ToDictionary<GuildConfig, ulong, IReadOnlyList<ulong>>(k => k.GuildId, v => v.GetAutoAssignableRoles())
.ToConcurrent();
_ = Task.Run(async () =>
{
while (true)
{
var user = await _assignQueue.Reader.ReadAsync();
if (!_autoAssignableRoles.TryGetValue(user.Guild.Id, out var savedRoleIds))
continue;
try
{
var roleIds = savedRoleIds
.Select(roleId => user.Guild.GetRole(roleId))
.Where(x => !(x is null))
.ToList();
if (roleIds.Any())
{
await user.AddRolesAsync(roleIds).ConfigureAwait(false);
await Task.Delay(250).ConfigureAwait(false);
}
else
{
Log.Warning(
"Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server the roles dont exist",
user.Guild.Name,
user.Guild.Id);
await DisableAarAsync(user.Guild.Id);
}
}
catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
Log.Warning("Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server because I don't have role management permissions",
user.Guild.Name,
user.Guild.Id);
await DisableAarAsync(user.Guild.Id);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in aar. Probably one of the roles doesn't exist");
}
}
});
_client.UserJoined += OnClientOnUserJoined;
_client.RoleDeleted += OnClientRoleDeleted;
}
private async Task OnClientRoleDeleted(SocketRole role)
{
if (_autoAssignableRoles.TryGetValue(role.Guild.Id, out var roles)
&& roles.Contains(role.Id))
{
await ToggleAarAsync(role.Guild.Id, role.Id);
}
}
private async Task OnClientOnUserJoined(SocketGuildUser user)
{
if (_autoAssignableRoles.TryGetValue(user.Guild.Id, out _))
await _assignQueue.Writer.WriteAsync(user);
}
public async Task<IReadOnlyList<ulong>> ToggleAarAsync(ulong guildId, ulong roleId)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigs.ForId(guildId, set => set);
var roles = gc.GetAutoAssignableRoles();
if(!roles.Remove(roleId) && roles.Count < 3)
roles.Add(roleId);
gc.SetAutoAssignableRoles(roles);
await uow.SaveChangesAsync();
if (roles.Count > 0)
_autoAssignableRoles[guildId] = roles;
else
_autoAssignableRoles.TryRemove(guildId, out _);
return roles;
}
public async Task DisableAarAsync(ulong guildId)
{
using var uow = _db.GetDbContext();
await uow._context
.GuildConfigs
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.UpdateAsync(_ => new GuildConfig(){ AutoAssignRoleIds = null});
_autoAssignableRoles.TryRemove(guildId, out _);
await uow.SaveChangesAsync();
}
public async Task SetAarRolesAsync(ulong guildId, IEnumerable<ulong> newRoles)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigs.ForId(guildId, set => set);
gc.SetAutoAssignableRoles(newRoles);
await uow.SaveChangesAsync();
}
public bool TryGetRoles(ulong guildId, out IReadOnlyList<ulong> roles)
=> _autoAssignableRoles.TryGetValue(guildId, out roles);
}
public static class GuildConfigExtensions
{
public static List<ulong> GetAutoAssignableRoles(this GuildConfig gc)
{
if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds))
return new List<ulong>();
return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList();
}
public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable<ulong> roles)
{
gc.AutoAssignRoleIds = roles.JoinWith(',');
}
}
}

View File

@@ -0,0 +1,145 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Services;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Core.Modules.Administration.Services
{
public class DangerousCommandsService : INService
{
public const string WaifusDeleteSql = @"DELETE FROM WaifuUpdates;
DELETE FROM WaifuItem;
DELETE FROM WaifuInfo;";
public const string WaifuDeleteSql = @"DELETE FROM WaifuUpdates WHERE UserId=(SELECT Id FROM DiscordUser WHERE UserId={0});
DELETE FROM WaifuItem WHERE WaifuInfoId=(SELECT Id FROM WaifuInfo WHERE WaifuId=(SELECT Id FROM DiscordUser WHERE UserId={0}));
UPDATE WaifuInfo SET ClaimerId=NULL WHERE ClaimerId=(SELECT Id FROM DiscordUser WHERE UserId={0});
DELETE FROM WaifuInfo WHERE WaifuId=(SELECT Id FROM DiscordUser WHERE UserId={0});";
public const string CurrencyDeleteSql = "UPDATE DiscordUser SET CurrencyAmount=0; DELETE FROM CurrencyTransactions; DELETE FROM PlantedCurrency;";
public const string MusicPlaylistDeleteSql = "DELETE FROM MusicPlaylists;";
public const string XpDeleteSql = @"DELETE FROM UserXpStats;
UPDATE DiscordUser
SET ClubId=NULL,
IsClubAdmin=0,
TotalXp=0;
DELETE FROM ClubApplicants;
DELETE FROM ClubBans;
DELETE FROM Clubs;";
// public const string DeleteUnusedCustomReactionsAndQuotes = @"DELETE FROM CustomReactions
//WHERE UseCount=0 AND (DateAdded < date('now', '-7 day') OR DateAdded is null);
//DELETE FROM Quotes
//WHERE UseCount=0 AND (DateAdded < date('now', '-7 day') OR DateAdded is null);";
private readonly DbService _db;
public DangerousCommandsService(DbService db)
{
_db = db;
}
public async Task<int> ExecuteSql(string sql)
{
int res;
using (var uow = _db.GetDbContext())
{
res = await uow._context.Database.ExecuteSqlRawAsync(sql);
}
return res;
}
public class SelectResult
{
public List<string> ColumnNames { get; set; }
public List<string[]> Results { get; set; }
}
public SelectResult SelectSql(string sql)
{
var result = new SelectResult()
{
ColumnNames = new List<string>(),
Results = new List<string[]>(),
};
using (var uow = _db.GetDbContext())
{
var conn = uow._context.Database.GetDbConnection();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = sql;
using (var reader = cmd.ExecuteReader())
{
if (reader.HasRows)
{
for (int i = 0; i < reader.FieldCount; i++)
{
result.ColumnNames.Add(reader.GetName(i));
}
while (reader.Read())
{
var obj = new object[reader.FieldCount];
reader.GetValues(obj);
result.Results.Add(obj.Select(x => x.ToString()).ToArray());
}
}
}
}
}
return result;
}
public async Task PurgeUserAsync(ulong userId)
{
using var uow = _db.GetDbContext();
// get waifu info
var wi = await uow._context.Set<WaifuInfo>()
.FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId);
// if it exists, delete waifu related things
if (!(wi is null))
{
// remove updates which have new or old as this waifu
await uow._context
.WaifuUpdates
.DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId);
// delete all items this waifu owns
await uow._context
.Set<WaifuItem>()
.DeleteAsync(x => x.WaifuInfoId == wi.Id);
// all waifus this waifu claims are released
await uow._context
.Set<WaifuInfo>()
.AsQueryable()
.Where(x => x.Claimer.UserId == userId)
.UpdateAsync(x => new WaifuInfo() {ClaimerId = null});
// all affinities set to this waifu are reset
await uow._context
.Set<WaifuInfo>()
.AsQueryable()
.Where(x => x.Affinity.UserId == userId)
.UpdateAsync(x => new WaifuInfo() {AffinityId = null});
}
// delete guild xp
await uow._context
.UserXpStats
.DeleteAsync(x => x.UserId == userId);
// delete currency transactions
await uow._context.Set<CurrencyTransaction>()
.DeleteAsync(x => x.UserId == userId);
// delete user, currency, and clubs go away with it
await uow._context.DiscordUser
.DeleteAsync(u => u.UserId == userId);
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Administration.Services
{
public class DiscordPermOverrideService : INService, ILateBlocker
{
private readonly DbService _db;
private readonly IServiceProvider _services;
public int Priority { get; } = int.MaxValue;
private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides;
public DiscordPermOverrideService(DbService db, IServiceProvider services)
{
_db = db;
_services = services;
using var uow = _db.GetDbContext();
_overrides = uow._context.DiscordPermOverrides
.AsNoTracking()
.AsEnumerable()
.ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o)
.ToConcurrent();
}
public bool TryGetOverrides(ulong guildId, string commandName, out GuildPerm? perm)
{
commandName = commandName.ToLowerInvariant();
if (_overrides.TryGetValue((guildId, commandName), out var dpo))
{
perm = dpo.Perm;
return true;
}
perm = null;
return false;
}
public Task<PreconditionResult> ExecuteOverrides(ICommandContext ctx, CommandInfo command,
GuildPerm perms, IServiceProvider services)
{
var rupa = new RequireUserPermissionAttribute((GuildPermission) perms);
return rupa.CheckPermissionsAsync(ctx, command, services);
}
public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm)
{
commandName = commandName.ToLowerInvariant();
using (var uow = _db.GetDbContext())
{
var over = await uow._context
.Set<DiscordPermOverride>()
.AsQueryable()
.FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
if (over is null)
{
uow._context.Set<DiscordPermOverride>()
.Add(over = new DiscordPermOverride()
{
Command = commandName,
Perm = perm,
GuildId = guildId,
});
}
else
{
over.Perm = perm;
}
_overrides[(guildId, commandName)] = over;
await uow.SaveChangesAsync();
}
}
public async Task ClearAllOverrides(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
var overrides = await uow._context
.Set<DiscordPermOverride>()
.AsQueryable()
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.ToListAsync();
uow._context.RemoveRange(overrides);
await uow.SaveChangesAsync();
foreach (var over in overrides)
{
_overrides.TryRemove((guildId, over.Command), out _);
}
}
}
public async Task RemoveOverride(ulong guildId, string commandName)
{
commandName = commandName.ToLowerInvariant();
using (var uow = _db.GetDbContext())
{
var over = await uow._context
.Set<DiscordPermOverride>()
.AsQueryable()
.AsNoTracking()
.FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName);
if (over is null)
return;
uow._context.Remove(over);
await uow.SaveChangesAsync();
_overrides.TryRemove((guildId, commandName), out _);
}
}
public Task<List<DiscordPermOverride>> GetAllOverrides(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
return uow._context
.Set<DiscordPermOverride>()
.AsQueryable()
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.ToListAsync();
}
}
public async Task<bool> TryBlockLate(DiscordSocketClient client, ICommandContext context, string moduleName,
CommandInfo command)
{
if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && !(perm is null))
{
var result = await new RequireUserPermissionAttribute((GuildPermission) perm)
.CheckPermissionsAsync(context, command, _services);
return !result.IsSuccess;
}
return false;
}
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.WebSocket;
using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public class GameVoiceChannelService : INService
{
public ConcurrentHashSet<ulong> GameVoiceChannels { get; } = new ConcurrentHashSet<ulong>();
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public GameVoiceChannelService(DiscordSocketClient client, DbService db, NadekoBot bot)
{
_db = db;
_client = client;
GameVoiceChannels = new ConcurrentHashSet<ulong>(
bot.AllGuildConfigs.Where(gc => gc.GameVoiceChannel != null)
.Select(gc => gc.GameVoiceChannel.Value));
_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated;
_client.GuildMemberUpdated += _client_GuildMemberUpdated;
}
private Task _client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after)
{
var _ = Task.Run(async () =>
{
try
{
//if the user is in the voice channel and that voice channel is gvc
var vc = after.VoiceChannel;
if (vc == null || !GameVoiceChannels.Contains(vc.Id))
return;
//if the activity has changed, and is a playing activity
if (before.Activity != after.Activity
&& after.Activity != null
&& after.Activity.Type == Discord.ActivityType.Playing)
{
//trigger gvc
await TriggerGvc(after, after.Activity.Name);
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error running GuildMemberUpdated in gvc");
}
});
return Task.CompletedTask;
}
public ulong? ToggleGameVoiceChannel(ulong guildId, ulong vchId)
{
ulong? id;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set);
if (gc.GameVoiceChannel == vchId)
{
GameVoiceChannels.TryRemove(vchId);
id = gc.GameVoiceChannel = null;
}
else
{
if (gc.GameVoiceChannel != null)
GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value);
GameVoiceChannels.Add(vchId);
id = gc.GameVoiceChannel = vchId;
}
uow.SaveChanges();
}
return id;
}
private Task Client_UserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState)
{
var _ = Task.Run(async () =>
{
try
{
if (!(usr is SocketGuildUser gUser))
return;
var game = gUser.Activity?.Name;
if (oldState.VoiceChannel == newState.VoiceChannel ||
newState.VoiceChannel == null)
return;
if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id) ||
string.IsNullOrWhiteSpace(game))
return;
await TriggerGvc(gUser, game);
}
catch (Exception ex)
{
Log.Warning(ex, "Error running VoiceStateUpdate in gvc");
}
});
return Task.CompletedTask;
}
private async Task TriggerGvc(SocketGuildUser gUser, string game)
{
if (string.IsNullOrWhiteSpace(game))
return;
game = game.TrimTo(50).ToLowerInvariant();
var vch = gUser.Guild.VoiceChannels
.FirstOrDefault(x => x.Name.ToLowerInvariant() == game);
if (vch == null)
return;
await Task.Delay(1000).ConfigureAwait(false);
await gUser.ModifyAsync(gu => gu.Channel = vch).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Administration.Services
{
public class GuildTimezoneService : INService
{
public static ConcurrentDictionary<ulong, GuildTimezoneService> AllServices { get; } = new ConcurrentDictionary<ulong, GuildTimezoneService>();
private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
private readonly DbService _db;
public GuildTimezoneService(DiscordSocketClient client, NadekoBot bot, DbService db)
{
_timezones = bot.AllGuildConfigs
.Select(GetTimzezoneTuple)
.Where(x => x.Timezone != null)
.ToDictionary(x => x.GuildId, x => x.Timezone)
.ToConcurrent();
var curUser = client.CurrentUser;
if (curUser != null)
AllServices.TryAdd(curUser.Id, this);
_db = db;
bot.JoinedGuild += Bot_JoinedGuild;
}
private Task Bot_JoinedGuild(GuildConfig arg)
{
var (guildId, tz) = GetTimzezoneTuple(arg);
if (tz != null)
_timezones.TryAdd(guildId, tz);
return Task.CompletedTask;
}
private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x)
{
TimeZoneInfo tz;
try
{
if (x.TimeZoneId == null)
tz = null;
else
tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId);
}
catch
{
tz = null;
}
return (x.GuildId, Timezone: tz);
}
public TimeZoneInfo GetTimeZoneOrDefault(ulong guildId)
{
if (_timezones.TryGetValue(guildId, out var tz))
return tz;
return null;
}
public void SetTimeZone(ulong guildId, TimeZoneInfo tz)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set);
gc.TimeZoneId = tz?.Id;
uow.SaveChanges();
if (tz == null)
_timezones.TryRemove(guildId, out tz);
else
_timezones.AddOrUpdate(guildId, tz, (key, old) => tz);
}
}
public TimeZoneInfo GetTimeZoneOrUtc(ulong guildId)
=> GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,477 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public enum MuteType
{
Voice,
Chat,
All
}
public class MuteService : INService
{
public ConcurrentDictionary<ulong, string> GuildMuteRoles { get; }
public ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> MutedUsers { get; }
public ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>> Un_Timers { get; }
= new ConcurrentDictionary<ulong, ConcurrentDictionary<(ulong, TimerType), Timer>>();
public event Action<IGuildUser, IUser, MuteType, string> UserMuted = delegate { };
public event Action<IGuildUser, IUser, MuteType, string> UserUnmuted = delegate { };
private static readonly OverwritePermissions denyOverwrite =
new OverwritePermissions(addReactions: PermValue.Deny, sendMessages: PermValue.Deny,
attachFiles: PermValue.Deny);
private readonly DiscordSocketClient _client;
private readonly DbService _db;
public MuteService(DiscordSocketClient client, DbService db)
{
_client = client;
_db = db;
using (var uow = db.GetDbContext())
{
var guildIds = client.Guilds.Select(x => x.Id).ToList();
var configs = uow._context.Set<GuildConfig>().AsQueryable()
.Include(x => x.MutedUsers)
.Include(x => x.UnbanTimer)
.Include(x => x.UnmuteTimers)
.Include(x => x.UnroleTimer)
.Where(x => guildIds.Contains(x.GuildId))
.ToList();
GuildMuteRoles = configs
.Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName))
.ToDictionary(c => c.GuildId, c => c.MuteRoleName)
.ToConcurrent();
MutedUsers = new ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>>(configs
.ToDictionary(
k => k.GuildId,
v => new ConcurrentHashSet<ulong>(v.MutedUsers.Select(m => m.UserId))
));
var max = TimeSpan.FromDays(49);
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
{
var unmute = x.UnmuteAt - DateTime.UtcNow;
after = unmute > max ? max : unmute;
}
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute);
}
foreach (var x in conf.UnbanTimer)
{
TimeSpan after;
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
{
after = TimeSpan.FromMinutes(2);
}
else
{
var unban = x.UnbanAt - DateTime.UtcNow;
after = unban > max ? max : unban;
}
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban);
}
foreach (var x in conf.UnroleTimer)
{
TimeSpan after;
if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow)
{
after = TimeSpan.FromMinutes(2);
}
else
{
var unban = x.UnbanAt - DateTime.UtcNow;
after = unban > max ? max : unban;
}
StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId);
}
}
_client.UserJoined += Client_UserJoined;
}
UserMuted += OnUserMuted;
UserUnmuted += OnUserUnmuted;
}
private void OnUserMuted(IGuildUser user, IUser mod, MuteType type, string reason)
{
if (string.IsNullOrWhiteSpace(reason))
return;
var _ = Task.Run(() => user.SendMessageAsync(embed: new EmbedBuilder()
.WithDescription($"You've been muted in {user.Guild} server")
.AddField("Mute Type", type.ToString())
.AddField("Moderator", mod.ToString())
.AddField("Reason", reason)
.Build()));
}
private void OnUserUnmuted(IGuildUser user, IUser mod, MuteType type, string reason)
{
if (string.IsNullOrWhiteSpace(reason))
return;
var _ = Task.Run(() => user.SendMessageAsync(embed: new EmbedBuilder()
.WithDescription($"You've been unmuted in {user.Guild} server")
.AddField("Unmute Type", type.ToString())
.AddField("Moderator", mod.ToString())
.AddField("Reason", reason)
.Build()));
}
private Task Client_UserJoined(IGuildUser usr)
{
try
{
MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet<ulong> muted);
if (muted == null || !muted.Contains(usr.Id))
return Task.CompletedTask;
var _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute").ConfigureAwait(false));
}
catch (Exception ex)
{
Log.Warning(ex, "Error in MuteService UserJoined event");
}
return Task.CompletedTask;
}
public async Task SetMuteRoleAsync(ulong guildId, string name)
{
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guildId, set => set);
config.MuteRoleName = name;
GuildMuteRoles.AddOrUpdate(guildId, name, (id, old) => name);
await uow.SaveChangesAsync();
}
}
public async Task MuteUser(IGuildUser usr, IUser mod, MuteType type = MuteType.All, string reason = "")
{
if (type == MuteType.All)
{
try { await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); } catch { }
var muteRole = await GetMuteRole(usr.Guild).ConfigureAwait(false);
if (!usr.RoleIds.Contains(muteRole.Id))
await usr.AddRoleAsync(muteRole).ConfigureAwait(false);
StopTimer(usr.GuildId, usr.Id, TimerType.Mute);
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(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<ulong> muted))
muted.Add(usr.Id);
config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id);
await uow.SaveChangesAsync();
}
UserMuted(usr, mod, MuteType.All, reason);
}
else if (type == MuteType.Voice)
{
try
{
await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false);
UserMuted(usr, mod, MuteType.Voice, reason);
}
catch { }
}
else if (type == MuteType.Chat)
{
await usr.AddRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false);
UserMuted(usr, mod, MuteType.Chat, reason);
}
}
public async Task UnmuteUser(ulong guildId, ulong usrId, IUser mod, MuteType type = MuteType.All, string reason = "")
{
var usr = _client.GetGuild(guildId)?.GetUser(usrId);
if (type == MuteType.All)
{
StopTimer(guildId, usrId, TimerType.Mute);
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guildId, set => set.Include(gc => gc.MutedUsers)
.Include(gc => gc.UnmuteTimers));
var match = new MutedUserId()
{
UserId = usrId
};
var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match));
if (toRemove != null)
{
uow._context.Remove(toRemove);
}
if (MutedUsers.TryGetValue(guildId, out ConcurrentHashSet<ulong> muted))
muted.TryRemove(usrId);
config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId);
await uow.SaveChangesAsync();
}
if (usr != null)
{
try { await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); } catch { }
try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); } catch { /*ignore*/ }
UserUnmuted(usr, mod, MuteType.All, reason);
}
}
else if (type == MuteType.Voice)
{
if (usr == null)
return;
try
{
await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false);
UserUnmuted(usr, mod, MuteType.Voice, reason);
}
catch { }
}
else if (type == MuteType.Chat)
{
if (usr == null)
return;
await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false);
UserUnmuted(usr, mod, MuteType.Chat, reason);
}
}
public async Task<IRole> GetMuteRole(IGuild guild)
{
if (guild == null)
throw new ArgumentNullException(nameof(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, isMentionable: false).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, isMentionable: false).ConfigureAwait(false);
}
}
foreach (var toOverwrite in (await guild.GetTextChannelsAsync().ConfigureAwait(false)))
{
try
{
if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id
&& x.TargetType == PermissionTarget.Role))
{
await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite)
.ConfigureAwait(false);
await Task.Delay(200).ConfigureAwait(false);
}
}
catch
{
// ignored
}
}
return muteRole;
}
public async Task TimedMute(IGuildUser user, IUser mod, TimeSpan after, MuteType muteType = MuteType.All, string reason = "")
{
await MuteUser(user, mod, muteType, reason).ConfigureAwait(false); // mute the user. This will also remove any previous unmute timers
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(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.SaveChanges();
}
StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer
}
public async Task TimedBan(IGuild guild, IUser user, TimeSpan after, string reason)
{
await guild.AddBanAsync(user.Id, 0, reason).ConfigureAwait(false);
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guild.Id, set => set.Include(x => x.UnbanTimer));
config.UnbanTimer.Add(new UnbanTimer()
{
UserId = user.Id,
UnbanAt = DateTime.UtcNow + after,
}); // add teh unmute timer to the database
uow.SaveChanges();
}
StartUn_Timer(guild.Id, user.Id, after, TimerType.Ban); // start the timer
}
public async Task TimedRole(IGuildUser user, TimeSpan after, string reason, IRole role)
{
await user.AddRoleAsync(role).ConfigureAwait(false);
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(user.GuildId, set => set.Include(x => x.UnroleTimer));
config.UnroleTimer.Add(new UnroleTimer()
{
UserId = user.Id,
UnbanAt = DateTime.UtcNow + after,
RoleId = role.Id
}); // add teh unmute timer to the database
uow.SaveChanges();
}
StartUn_Timer(user.GuildId, user.Id, after, TimerType.AddRole, role.Id); // start the timer
}
public enum TimerType { Mute, Ban, AddRole }
public void StartUn_Timer(ulong guildId, ulong userId, TimeSpan after, TimerType type, ulong? roleId = null)
{
//load the unmute timers for this guild
var userUnTimers = Un_Timers.GetOrAdd(guildId, new ConcurrentDictionary<(ulong, TimerType), Timer>());
//unmute timer to be added
var toAdd = new Timer(async _ =>
{
if (type == TimerType.Ban)
{
try
{
RemoveTimerFromDb(guildId, userId, type);
StopTimer(guildId, userId, type);
var guild = _client.GetGuild(guildId); // load the guild
if (guild != null)
{
await guild.RemoveBanAsync(userId).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Warning(ex, "Couldn't unban user {0} in guild {1}", userId, guildId);
}
}
else if (type == TimerType.AddRole)
{
try
{
RemoveTimerFromDb(guildId, userId, type);
StopTimer(guildId, userId, type);
var guild = _client.GetGuild(guildId);
var user = guild?.GetUser(userId);
var role = guild.GetRole(roleId.Value);
if (guild != null && user != null && user.Roles.Contains(role))
{
await user.RemoveRoleAsync(role).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Warning(ex, "Couldn't remove role from user {0} in guild {1}", userId, guildId);
}
}
else
{
try
{
// unmute the user, this will also remove the timer from the db
await UnmuteUser(guildId, userId, _client.CurrentUser, reason: "Timed mute expired").ConfigureAwait(false);
}
catch (Exception ex)
{
RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db
Log.Warning(ex, "Couldn't unmute user {0} in guild {1}", userId, guildId);
}
}
}, null, after, Timeout.InfiniteTimeSpan);
//add it, or stop the old one and add this one
userUnTimers.AddOrUpdate((userId, type), (key) => toAdd, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return toAdd;
});
}
public void StopTimer(ulong guildId, ulong userId, TimerType type)
{
if (!Un_Timers.TryGetValue(guildId, out ConcurrentDictionary<(ulong, TimerType), Timer> userTimer))
return;
if (userTimer.TryRemove((userId, type), out Timer removed))
{
removed.Change(Timeout.Infinite, Timeout.Infinite);
}
}
private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type)
{
using (var uow = _db.GetDbContext())
{
object toDelete;
if (type == TimerType.Mute)
{
var config = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.UnmuteTimers));
toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId);
}
else
{
var config = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.UnbanTimer));
toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId);
}
if (toDelete != null)
{
uow._context.Remove(toDelete);
}
uow.SaveChanges();
}
}
}
}

View File

@@ -0,0 +1,31 @@
//using NadekoBot.Core.Services;
//using System;
//using System.Collections.Generic;
//using System.IO;
//using System.Linq;
//using System.Text.RegularExpressions;
//namespace NadekoBot.Modules.Administration.Services
//{
// public class PackagesService : INService
// {
// public IEnumerable<string> Packages { get; private set; }
// public PackagesService()
// {
// ReloadAvailablePackages();
// }
// public void ReloadAvailablePackages()
// {
// Packages = Directory.GetDirectories(Path.Combine(Appctx.BaseDirectory, "modules\\"), "NadekoBot.Modules.*", SearchOption.AllDirectories)
// .SelectMany(x => Directory.GetFiles(x, "NadekoBot.Modules.*.dll"))
// .Select(x => Path.GetFileNameWithoutExtension(x))
// .Select(x =>
// {
// var m = Regex.Match(x, @"NadekoBot\.Modules\.(?<name>.*)");
// return m.Groups["name"].Value;
// });
// }
// }
//}

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Discord.WebSocket;
using NadekoBot.Common.Replacements;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using Discord;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Common;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public sealed class PlayingRotateService : INService
{
private readonly Timer _t;
private readonly BotConfigService _bss;
private readonly Replacer _rep;
private readonly DbService _db;
private readonly NadekoBot _bot;
private class TimerState
{
public int Index { get; set; }
}
public PlayingRotateService(DiscordSocketClient client, DbService db, NadekoBot bot,
BotConfigService bss, IEnumerable<IPlaceholderProvider> phProviders)
{
_db = db;
_bot = bot;
_bss = bss;
if (client.ShardId == 0)
{
_rep = new ReplacementBuilder()
.WithClient(client)
.WithProviders(phProviders)
.Build();
_t = new Timer(RotatingStatuses, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
}
private async void RotatingStatuses(object objState)
{
try
{
var state = (TimerState) objState;
if (!_bss.Data.RotateStatuses) return;
IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
using (var uow = _db.GetDbContext())
{
rotatingStatuses = uow._context.RotatingStatus
.AsNoTracking()
.OrderBy(x => x.Id)
.ToList();
}
if (rotatingStatuses.Count == 0)
return;
var playingStatus = state.Index >= rotatingStatuses.Count
? rotatingStatuses[state.Index = 0]
: rotatingStatuses[state.Index++];
var statusText = _rep.Replace(playingStatus.Status);
await _bot.SetGameAsync(statusText, playingStatus.Type);
}
catch (Exception ex)
{
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
}
}
public async Task<string> RemovePlayingAsync(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
using var uow = _db.GetDbContext();
var toRemove = await uow._context.RotatingStatus
.AsQueryable()
.AsNoTracking()
.Skip(index)
.FirstOrDefaultAsync();
if (toRemove is null)
return null;
uow._context.Remove(toRemove);
await uow.SaveChangesAsync();
return toRemove.Status;
}
public async Task AddPlaying(ActivityType t, string status)
{
using var uow = _db.GetDbContext();
var toAdd = new RotatingPlayingStatus {Status = status, Type = t};
uow._context.Add(toAdd);
await uow.SaveChangesAsync();
}
public bool ToggleRotatePlaying()
{
var enabled = false;
_bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
return enabled;
}
public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
{
using var uow = _db.GetDbContext();
return uow._context.RotatingStatus.AsNoTracking().ToList();
}
}
}

View File

@@ -0,0 +1,487 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Modules.Administration.Common;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Common.TypeReaders.Models;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public class ProtectionService : INService
{
private readonly ConcurrentDictionary<ulong, AntiRaidStats> _antiRaidGuilds
= new ConcurrentDictionary<ulong, AntiRaidStats>();
private readonly ConcurrentDictionary<ulong, AntiSpamStats> _antiSpamGuilds
= new ConcurrentDictionary<ulong, AntiSpamStats>();
private readonly ConcurrentDictionary<ulong, AntiAltStats> _antiAltGuilds
= new ConcurrentDictionary<ulong, AntiAltStats>();
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered
= delegate { return Task.CompletedTask; };
private readonly DiscordSocketClient _client;
private readonly MuteService _mute;
private readonly DbService _db;
private readonly UserPunishService _punishService;
private readonly Channel<PunishQueueItem> PunishUserQueue =
System.Threading.Channels.Channel.CreateUnbounded<PunishQueueItem>(new UnboundedChannelOptions()
{
SingleReader = true,
SingleWriter = false
});
public ProtectionService(DiscordSocketClient client, NadekoBot bot,
MuteService mute, DbService db, UserPunishService punishService)
{
_client = client;
_mute = mute;
_db = db;
_punishService = punishService;
var ids = client.GetGuildIds();
using (var uow = db.GetDbContext())
{
var configs = uow._context.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.AntiRaidSetting)
.Include(x => x.AntiSpamSetting)
.ThenInclude(x => x.IgnoredChannels)
.Include(x => x.AntiAltSetting)
.Where(x => ids.Contains(x.GuildId))
.ToList();
foreach (var gc in configs)
{
Initialize(gc);
}
}
_client.MessageReceived += HandleAntiSpam;
_client.UserJoined += HandleUserJoined;
bot.JoinedGuild += _bot_JoinedGuild;
_client.LeftGuild += _client_LeftGuild;
_ = Task.Run(RunQueue);
}
private async Task RunQueue()
{
while (true)
{
var item = await PunishUserQueue.Reader.ReadAsync();
var muteTime = item.MuteTime;
var gu = item.User;
try
{
await _punishService.ApplyPunishment(gu.Guild, gu, _client.CurrentUser,
item.Action, muteTime, item.RoleId, $"{item.Type} Protection");
}
catch (Exception ex)
{
Log.Warning(ex, "Error in punish queue: {Message}", ex.Message);
}
finally
{
await Task.Delay(1000);
}
}
}
private Task _client_LeftGuild(SocketGuild guild)
{
var _ = Task.Run(async () =>
{
TryStopAntiRaid(guild.Id);
TryStopAntiSpam(guild.Id);
await TryStopAntiAlt(guild.Id);
});
return Task.CompletedTask;
}
private Task _bot_JoinedGuild(GuildConfig gc)
{
using var uow = _db.GetDbContext();
var gcWithData = uow.GuildConfigs.ForId(gc.GuildId,
set => set
.Include(x => x.AntiRaidSetting)
.Include(x => x.AntiAltSetting)
.Include(x => x.AntiSpamSetting)
.ThenInclude(x => x.IgnoredChannels));
Initialize(gcWithData);
return Task.CompletedTask;
}
private void Initialize(GuildConfig gc)
{
var raid = gc.AntiRaidSetting;
var spam = gc.AntiSpamSetting;
if (raid != null)
{
var raidStats = new AntiRaidStats() { AntiRaidSettings = raid };
_antiRaidGuilds[gc.GuildId] = raidStats;
}
if (spam != null)
_antiSpamGuilds[gc.GuildId] = new AntiSpamStats() { AntiSpamSettings = spam };
var alt = gc.AntiAltSetting;
if (!(alt is null))
_antiAltGuilds[gc.GuildId] = new AntiAltStats(alt);
}
private Task HandleUserJoined(SocketGuildUser user)
{
if (user.IsBot)
return Task.CompletedTask;
_antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats);
_antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts);
if (maybeStats is null && maybeAlts is null)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
if (maybeAlts is AntiAltStats alts)
{
if (user.CreatedAt != default)
{
var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime;
if (diff < alts.MinAge)
{
alts.Increment();
await PunishUsers(
alts.Action,
ProtectionType.Alting,
alts.ActionDurationMinutes,
alts.RoleId,
user);
return;
}
}
}
try
{
if (!(maybeStats is AntiRaidStats stats) || !stats.RaidUsers.Add(user))
return;
++stats.UsersCount;
if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold)
{
var users = stats.RaidUsers.ToArray();
stats.RaidUsers.Clear();
var settings = stats.AntiRaidSettings;
await PunishUsers(settings.Action, ProtectionType.Raiding,
settings.PunishDuration, null, users).ConfigureAwait(false);
}
await Task.Delay(1000 * stats.AntiRaidSettings.Seconds).ConfigureAwait(false);
stats.RaidUsers.TryRemove(user);
--stats.UsersCount;
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
private Task HandleAntiSpam(SocketMessage arg)
{
if (!(arg is SocketUserMessage msg) || msg.Author.IsBot)
return Task.CompletedTask;
if (!(msg.Channel is ITextChannel channel))
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
try
{
if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) ||
spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore()
{
ChannelId = channel.Id
}))
return;
var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, (id) => new UserSpamStats(msg),
(id, old) =>
{
old.ApplyNextMessage(msg); return old;
});
if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold)
{
if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats))
{
stats.Dispose();
var settings = spamSettings.AntiSpamSettings;
await PunishUsers(settings.Action, ProtectionType.Spamming, settings.MuteTime,
settings.RoleId, (IGuildUser)msg.Author)
.ConfigureAwait(false);
}
}
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
private async Task PunishUsers(PunishmentAction action, ProtectionType pt, int muteTime, ulong? roleId,
params IGuildUser[] gus)
{
Log.Information(
"[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild",
pt,
gus.Length,
action,
gus[0].Guild.Name);
foreach (var gu in gus)
{
await PunishUserQueue.Writer.WriteAsync(new PunishQueueItem()
{
Action = action,
Type = pt,
User = gu,
MuteTime = muteTime,
RoleId = roleId
});
}
_ = OnAntiProtectionTriggered(action, pt, gus);
}
public async Task<AntiRaidStats> StartAntiRaidAsync(ulong guildId, int userThreshold, int seconds,
PunishmentAction action, int minutesDuration)
{
var g = _client.GetGuild(guildId);
await _mute.GetMuteRole(g).ConfigureAwait(false);
if (action == PunishmentAction.AddRole)
return null;
if (!IsDurationAllowed(action))
minutesDuration = 0;
var stats = new AntiRaidStats()
{
AntiRaidSettings = new AntiRaidSetting()
{
Action = action,
Seconds = seconds,
UserThreshold = userThreshold,
PunishDuration = minutesDuration
}
};
_antiRaidGuilds.AddOrUpdate(guildId, stats, (key, old) => stats);
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiRaidSetting));
gc.AntiRaidSetting = stats.AntiRaidSettings;
await uow.SaveChangesAsync();
}
return stats;
}
public bool TryStopAntiRaid(ulong guildId)
{
if (_antiRaidGuilds.TryRemove(guildId, out _))
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiRaidSetting));
gc.AntiRaidSetting = null;
uow.SaveChanges();
}
return true;
}
return false;
}
public bool TryStopAntiSpam(ulong guildId)
{
if (_antiSpamGuilds.TryRemove(guildId, out var removed))
{
removed.UserStats.ForEach(x => x.Value.Dispose());
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiSpamSetting)
.ThenInclude(x => x.IgnoredChannels));
gc.AntiSpamSetting = null;
uow.SaveChanges();
}
return true;
}
return false;
}
public async Task<AntiSpamStats> StartAntiSpamAsync(ulong guildId, int messageCount, PunishmentAction action,
int punishDurationMinutes, ulong? roleId)
{
var g = _client.GetGuild(guildId);
await _mute.GetMuteRole(g).ConfigureAwait(false);
if (!IsDurationAllowed(action))
punishDurationMinutes = 0;
var stats = new AntiSpamStats
{
AntiSpamSettings = new AntiSpamSetting()
{
Action = action,
MessageThreshold = messageCount,
MuteTime = punishDurationMinutes,
RoleId = roleId,
}
};
stats = _antiSpamGuilds.AddOrUpdate(guildId, stats, (key, old) =>
{
stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels;
return stats;
});
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiSpamSetting));
if (gc.AntiSpamSetting != null)
{
gc.AntiSpamSetting.Action = stats.AntiSpamSettings.Action;
gc.AntiSpamSetting.MessageThreshold = stats.AntiSpamSettings.MessageThreshold;
gc.AntiSpamSetting.MuteTime = stats.AntiSpamSettings.MuteTime;
gc.AntiSpamSetting.RoleId = stats.AntiSpamSettings.RoleId;
}
else
{
gc.AntiSpamSetting = stats.AntiSpamSettings;
}
await uow.SaveChangesAsync();
}
return stats;
}
public async Task<bool?> AntiSpamIgnoreAsync(ulong guildId, ulong channelId)
{
var obj = new AntiSpamIgnore()
{
ChannelId = channelId
};
bool added;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels));
var spam = gc.AntiSpamSetting;
if (spam is null)
{
return null;
}
if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful
{
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache
added = true;
}
else
{
var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId);
uow._context.Set<AntiSpamIgnore>().Remove(toRemove); // remove from db
if (_antiSpamGuilds.TryGetValue(guildId, out var temp))
{
temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache
}
added = false;
}
await uow.SaveChangesAsync();
}
return added;
}
public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId)
{
_antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats);
_antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats);
_antiAltGuilds.TryGetValue(guildId, out var antiAltStats);
return (antiSpamStats, antiRaidStats, antiAltStats);
}
public bool IsDurationAllowed(PunishmentAction action)
{
switch (action)
{
case PunishmentAction.Ban:
case PunishmentAction.Mute:
case PunishmentAction.ChatMute:
case PunishmentAction.VoiceMute:
case PunishmentAction.AddRole:
return true;
default:
return false;
}
}
public async Task StartAntiAltAsync(ulong guildId, int minAgeMinutes, PunishmentAction action,
int actionDurationMinutes = 0, ulong? roleId = null)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiAltSetting));
gc.AntiAltSetting = new AntiAltSetting()
{
Action = action,
ActionDurationMinutes = actionDurationMinutes,
MinAge = TimeSpan.FromMinutes(minAgeMinutes),
RoleId = roleId,
};
await uow.SaveChangesAsync();
_antiAltGuilds[guildId] = new AntiAltStats(gc.AntiAltSetting);
}
public async Task<bool> TryStopAntiAlt(ulong guildId)
{
if (!_antiAltGuilds.TryRemove(guildId, out _))
return false;
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.AntiAltSetting));
gc.AntiAltSetting = null;
await uow.SaveChangesAsync();
return true;
}
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
namespace NadekoBot.Modules.Administration.Services
{
public class PruneService : INService
{
//channelids where prunes are currently occuring
private ConcurrentHashSet<ulong> _pruningGuilds = new ConcurrentHashSet<ulong>();
private readonly TimeSpan twoWeeks = TimeSpan.FromDays(14);
private readonly LogCommandService _logService;
public PruneService(LogCommandService logService)
{
this._logService = logService;
}
public async Task PruneWhere(ITextChannel channel, int amount, Func<IMessage, bool> predicate)
{
channel.ThrowIfNull(nameof(channel));
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!_pruningGuilds.Add(channel.GuildId))
return;
try
{
IMessage[] msgs;
IMessage lastMessage = null;
msgs = (await channel.GetMessagesAsync(50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray();
while (amount > 0 && msgs.Any())
{
lastMessage = msgs[msgs.Length - 1];
var bulkDeletable = new List<IMessage>();
var singleDeletable = new List<IMessage>();
foreach (var x in msgs)
{
_logService.AddDeleteIgnore(x.Id);
if (DateTime.UtcNow - x.CreatedAt < twoWeeks)
bulkDeletable.Add(x);
else
singleDeletable.Add(x);
}
if (bulkDeletable.Count > 0)
await Task.WhenAll(Task.Delay(1000), channel.DeleteMessagesAsync(bulkDeletable)).ConfigureAwait(false);
var i = 0;
foreach (var group in singleDeletable.GroupBy(x => ++i / (singleDeletable.Count / 5)))
await Task.WhenAll(Task.Delay(1000), Task.WhenAll(group.Select(x => x.DeleteAsync()))).ConfigureAwait(false);
//this isn't good, because this still work as if i want to remove only specific user's messages from the last
//100 messages, Maybe this needs to be reduced by msgs.Length instead of 100
amount -= 50;
if(amount > 0)
msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray();
}
}
catch
{
//ignore
}
finally
{
_pruningGuilds.TryRemove(channel.GuildId);
}
}
}
}

View File

@@ -0,0 +1,190 @@
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public class RoleCommandsService : INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
public RoleCommandsService(DiscordSocketClient client, DbService db,
NadekoBot bot)
{
_db = db;
_client = client;
#if !GLOBAL_NADEKO
_models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId,
x => x.ReactionRoleMessages)
.ToConcurrent();
_client.ReactionAdded += _client_ReactionAdded;
_client.ReactionRemoved += _client_ReactionRemoved;
#endif
}
private Task _client_ReactionAdded(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
{
var _ = Task.Run(async () =>
{
try
{
if (!reaction.User.IsSpecified ||
reaction.User.Value.IsBot ||
!(reaction.User.Value is SocketGuildUser gusr))
return;
if (!(chan is SocketGuildChannel gch))
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf == null)
return;
// compare emote names for backwards compatibility :facepalm:
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole != null)
{
if (conf.Exclusive)
{
var roleIds = conf.ReactionRoles.Select(x => x.RoleId)
.Where(x => x != reactionRole.RoleId)
.Select(x => gusr.Guild.GetRole(x))
.Where(x => x != null);
var __ = Task.Run(async () =>
{
try
{
//if the role is exclusive,
// remove all other reactions user added to the message
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
foreach (var r in dl.Reactions)
{
if (r.Key.Name == reaction.Emote.Name)
continue;
try { await dl.RemoveReactionAsync(r.Key, gusr).ConfigureAwait(false); } catch { }
await Task.Delay(100).ConfigureAwait(false);
}
}
catch { }
});
await gusr.RemoveRolesAsync(roleIds).ConfigureAwait(false);
}
var toAdd = gusr.Guild.GetRole(reactionRole.RoleId);
if (toAdd != null && !gusr.Roles.Contains(toAdd))
{
await gusr.AddRolesAsync(new[] { toAdd }).ConfigureAwait(false);
}
}
else
{
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
new RequestOptions()
{
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
}).ConfigureAwait(false);
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
}
}
catch { }
});
return Task.CompletedTask;
}
private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
{
var _ = Task.Run(async () =>
{
try
{
if (!reaction.User.IsSpecified ||
reaction.User.Value.IsBot ||
!(reaction.User.Value is SocketGuildUser gusr))
return;
if (!(chan is SocketGuildChannel gch))
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf == null)
return;
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole != null)
{
var role = gusr.Guild.GetRole(reactionRole.RoleId);
if (role == null)
return;
await gusr.RemoveRoleAsync(role).ConfigureAwait(false);
}
}
catch { }
});
return Task.CompletedTask;
}
public bool Get(ulong id, out IndexedCollection<ReactionRoleMessage> rrs)
{
return _models.TryGetValue(id, out rrs);
}
public bool Add(ulong id, ReactionRoleMessage rrm)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(id, set => set
.Include(x => x.ReactionRoleMessages)
.ThenInclude(x => x.ReactionRoles));
if (gc.ReactionRoleMessages.Count >= 10)
return false;
gc.ReactionRoleMessages.Add(rrm);
_models.AddOrUpdate(id,
gc.ReactionRoleMessages,
delegate { return gc.ReactionRoleMessages; });
uow.SaveChanges();
}
return true;
}
public void Remove(ulong id, int index)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(id,
set => set.Include(x => x.ReactionRoleMessages)
.ThenInclude(x => x.ReactionRoles));
uow._context.Set<ReactionRole>()
.RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles);
gc.ReactionRoleMessages.RemoveAt(index);
_models.AddOrUpdate(id,
gc.ReactionRoleMessages,
delegate { return gc.ReactionRoleMessages; });
uow.SaveChanges();
}
}
}
}

View File

@@ -0,0 +1,270 @@
using Discord;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Modules.Xp;
namespace NadekoBot.Core.Modules.Administration.Services
{
public class SelfAssignedRolesService : INService
{
private readonly DbService _db;
public enum RemoveResult
{
Removed, // successfully removed
Err_Not_Assignable, // not assignable (error)
Err_Not_Have, // you don't have a role you want to remove (error)
Err_Not_Perms, // bot doesn't have perms (error)
}
public enum AssignResult
{
Assigned, // successfully removed
Err_Not_Assignable, // not assignable (error)
Err_Already_Have, // you already have that role (error)
Err_Not_Perms, // bot doesn't have perms (error)
Err_Lvl_Req, // you are not required level (error)
}
public SelfAssignedRolesService(DbService db)
{
_db = db;
}
public bool AddNew(ulong guildId, IRole role, int group)
{
using (var uow = _db.GetDbContext())
{
var roles = uow.SelfAssignedRoles.GetFromGuild(guildId);
if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
{
return false;
}
uow.SelfAssignedRoles.Add(new SelfAssignedRole
{
Group = group,
RoleId = role.Id,
GuildId = role.Guild.Id
});
uow.SaveChanges();
}
return true;
}
public bool ToggleAdSarm(ulong guildId)
{
bool newval;
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guildId, set => set);
newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages;
uow.SaveChanges();
}
return newval;
}
public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
{
LevelStats userLevelData;
using (var uow = _db.GetDbContext())
{
var stats = uow.Xp.GetOrCreateUser(guildUser.Guild.Id, guildUser.Id);
userLevelData = new LevelStats(stats.Xp + stats.AwardedXp);
}
var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id);
var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id);
if (theRoleYouWant == null)
{
return (AssignResult.Err_Not_Assignable, autoDelete, null);
}
else if (theRoleYouWant.LevelRequirement > userLevelData.Level)
{
return (AssignResult.Err_Lvl_Req, autoDelete, theRoleYouWant.LevelRequirement);
}
else if (guildUser.RoleIds.Contains(role.Id))
{
return (AssignResult.Err_Already_Have, autoDelete, null);
}
var roleIds = roles
.Where(x => x.Group == theRoleYouWant.Group)
.Select(x => x.RoleId).ToArray();
if (exclusive)
{
var sameRoles = guildUser.RoleIds
.Where(r => roleIds.Contains(r));
foreach (var roleId in sameRoles)
{
var sameRole = guildUser.Guild.GetRole(roleId);
if (sameRole != null)
{
try
{
await guildUser.RemoveRoleAsync(sameRole).ConfigureAwait(false);
await Task.Delay(300).ConfigureAwait(false);
}
catch
{
// ignored
}
}
}
}
try
{
await guildUser.AddRoleAsync(role).ConfigureAwait(false);
}
catch (Exception ex)
{
return (AssignResult.Err_Not_Perms, autoDelete, ex);
}
return (AssignResult.Assigned, autoDelete, null);
}
public async Task<bool> SetNameAsync(ulong guildId, int group, string name)
{
bool set = false;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames));
var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group);
if (string.IsNullOrWhiteSpace(name))
{
if (toUpdate != null)
gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
}
else if (toUpdate == null)
{
gc.SelfAssignableRoleGroupNames.Add(new GroupName
{
Name = name,
Number = group,
});
set = true;
}
else
{
toUpdate.Name = name;
set = true;
}
await uow.SaveChangesAsync();
}
return set;
}
public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role)
{
var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id);
if (roles.FirstOrDefault(r => r.RoleId == role.Id) == null)
{
return (RemoveResult.Err_Not_Assignable, autoDelete);
}
if (!guildUser.RoleIds.Contains(role.Id))
{
return (RemoveResult.Err_Not_Have, autoDelete);
}
try
{
await guildUser.RemoveRoleAsync(role).ConfigureAwait(false);
}
catch (Exception)
{
return (RemoveResult.Err_Not_Perms, autoDelete);
}
return (RemoveResult.Removed, autoDelete);
}
public bool RemoveSar(ulong guildId, ulong roleId)
{
bool success;
using (var uow = _db.GetDbContext())
{
success = uow.SelfAssignedRoles.DeleteByGuildAndRoleId(guildId, roleId);
uow.SaveChanges();
}
return success;
}
public (bool AutoDelete, bool Exclusive, IEnumerable<SelfAssignedRole>) GetAdAndRoles(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set);
var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages;
var exclusive = gc.ExclusiveSelfAssignedRoles;
var roles = uow.SelfAssignedRoles.GetFromGuild(guildId);
return (autoDelete, exclusive, roles);
}
}
public bool SetLevelReq(ulong guildId, IRole role, int level)
{
using (var uow = _db.GetDbContext())
{
var roles = uow.SelfAssignedRoles.GetFromGuild(guildId);
var sar = roles.FirstOrDefault(x => x.RoleId == role.Id);
if (sar != null)
{
sar.LevelRequirement = level;
uow.SaveChanges();
}
else
{
return false;
}
}
return true;
}
public bool ToggleEsar(ulong guildId)
{
bool areExclusive;
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guildId, set => set);
areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles;
uow.SaveChanges();
}
return areExclusive;
}
public (bool Exclusive, IEnumerable<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string> GroupNames) GetRoles(IGuild guild)
{
var exclusive = false;
IEnumerable<(SelfAssignedRole Model, IRole Role)> roles;
IDictionary<int, string> groupNames;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames));
exclusive = gc.ExclusiveSelfAssignedRoles;
groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name);
var roleModels = uow.SelfAssignedRoles.GetFromGuild(guild.Id);
roles = roleModels
.Select(x => (Model: x, Role: guild.GetRole(x.RoleId)));
uow.SelfAssignedRoles.RemoveRange(roles.Where(x => x.Role == null).Select(x => x.Model).ToArray());
uow.SaveChanges();
}
return (exclusive, roles.Where(x => x.Role != null), groupNames);
}
}
}

View File

@@ -0,0 +1,436 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using StackExchange.Redis;
using System.Collections.Generic;
using System.Diagnostics;
using Newtonsoft.Json;
using NadekoBot.Common.ShardCom;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Services.Database.Models;
using System.Threading;
using System.Collections.Concurrent;
using System;
using System.Net.Http;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
{
private readonly ConnectionMultiplexer _redis;
private readonly CommandHandler _cmdHandler;
private readonly DbService _db;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> _autoCommands =
new ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>>();
private readonly IDataCache _cache;
private readonly IImageCache _imgs;
private readonly IHttpClientFactory _httpFactory;
private readonly BotConfigService _bss;
public SelfService(DiscordSocketClient client, CommandHandler cmdHandler, DbService db,
IBotStrings strings, IBotCredentials creds, IDataCache cache, IHttpClientFactory factory,
BotConfigService bss)
{
_redis = cache.Redis;
_cmdHandler = cmdHandler;
_db = db;
_strings = strings;
_client = client;
_creds = creds;
_cache = cache;
_imgs = cache.LocalImages;
_httpFactory = factory;
_bss = bss;
var sub = _redis.GetSubscriber();
if (_client.ShardId == 0)
{
sub.Subscribe(_creds.RedisKey() + "_reload_images",
delegate { _imgs.Reload(); }, CommandFlags.FireAndForget);
}
sub.Subscribe(_creds.RedisKey() + "_leave_guild", async (ch, v) =>
{
try
{
var guildStr = v.ToString()?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(guildStr))
return;
var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr) ??
_client.Guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == guildStr);
if (server == null)
{
return;
}
if (server.OwnerId != _client.CurrentUser.Id)
{
await server.LeaveAsync().ConfigureAwait(false);
Log.Information($"Left server {server.Name} [{server.Id}]");
}
else
{
await server.DeleteAsync().ConfigureAwait(false);
Log.Information($"Deleted server {server.Name} [{server.Id}]");
}
}
catch
{
}
}, CommandFlags.FireAndForget);
}
public async Task OnReadyAsync()
{
using var uow = _db.GetDbContext();
_autoCommands = uow._context
.AutoCommands
.AsNoTracking()
.Where(x => x.Interval >= 5)
.AsEnumerable()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key,
y => y.ToDictionary(x => x.Id, TimerFromAutoCommand)
.ToConcurrent())
.ToConcurrent();
var startupCommands = uow._context.AutoCommands.AsNoTracking().Where(x => x.Interval == 0);
foreach (var cmd in startupCommands)
{
try
{
await ExecuteCommand(cmd).ConfigureAwait(false);
}
catch
{
}
}
if (_client.ShardId == 0)
{
await LoadOwnerChannels().ConfigureAwait(false);
}
}
private Timer TimerFromAutoCommand(AutoCommand x)
{
return new Timer(async (obj) => await ExecuteCommand((AutoCommand) obj).ConfigureAwait(false),
x,
x.Interval * 1000,
x.Interval * 1000);
}
private async Task ExecuteCommand(AutoCommand cmd)
{
try
{
if (cmd.GuildId is null)
return;
var guildShard = (int) ((cmd.GuildId.Value >> 22) % (ulong) _creds.TotalShards);
if (guildShard != _client.ShardId)
return;
var prefix = _cmdHandler.GetPrefix(cmd.GuildId);
//if someone already has .die as their startup command, ignore it
if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture))
return;
await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in SelfService ExecuteCommand");
}
}
public void AddNewAutoCommand(AutoCommand cmd)
{
using (var uow = _db.GetDbContext())
{
uow._context.AutoCommands.Add(cmd);
uow.SaveChanges();
}
if (cmd.Interval >= 5)
{
var autos = _autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary<int, Timer>());
autos.AddOrUpdate(cmd.Id, key => TimerFromAutoCommand(cmd), (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return TimerFromAutoCommand(cmd);
});
}
}
public IEnumerable<AutoCommand> GetStartupCommands()
{
using var uow = _db.GetDbContext();
return uow._context
.AutoCommands
.AsNoTracking()
.Where(x => x.Interval == 0)
.OrderBy(x => x.Id)
.ToList();
}
public IEnumerable<AutoCommand> GetAutoCommands()
{
using var uow = _db.GetDbContext();
return uow._context
.AutoCommands
.AsNoTracking()
.Where(x => x.Interval >= 5)
.OrderBy(x => x.Id)
.ToList();
}
private async Task LoadOwnerChannels()
{
var channels = await Task.WhenAll(_creds.OwnerIds.Select(id =>
{
var user = _client.GetUser(id);
if (user == null)
return Task.FromResult<IDMChannel>(null);
return user.GetOrCreateDMChannelAsync();
})).ConfigureAwait(false);
ownerChannels = channels.Where(x => x != null)
.ToDictionary(x => x.Recipient.Id, x => x)
.ToImmutableDictionary();
if (!ownerChannels.Any())
Log.Warning(
"No owner channels created! Make sure you've specified the correct OwnerId in the credentials.json file and invited the bot to a Discord server.");
else
Log.Information($"Created {ownerChannels.Count} out of {_creds.OwnerIds.Length} owner message channels.");
}
public Task LeaveGuild(string guildStr)
{
var sub = _cache.Redis.GetSubscriber();
return sub.PublishAsync(_creds.RedisKey() + "_leave_guild", guildStr);
}
// forwards dms
public async Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg)
{
var bs = _bss.Data;
if (msg.Channel is IDMChannel && _bss.Data.ForwardMessages && ownerChannels.Any())
{
var title = _strings.GetText("dm_from") +
$" [{msg.Author}]({msg.Author.Id})";
var attachamentsTxt = _strings.GetText("attachments");
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 (bs.ForwardToAllOwners)
{
var allOwnerChannels = ownerChannels.Values;
foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id))
{
try
{
await ownerCh.SendConfirmAsync(title, toSend).ConfigureAwait(false);
}
catch
{
Log.Warning("Can't contact owner with id {0}", ownerCh.Recipient.Id);
}
}
}
else
{
var firstOwnerChannel = ownerChannels.Values.First();
if (firstOwnerChannel.Recipient.Id != msg.Author.Id)
{
try
{
await firstOwnerChannel.SendConfirmAsync(title, toSend).ConfigureAwait(false);
}
catch
{
// ignored
}
}
}
}
}
public bool RestartBot()
{
var cmd = _creds.RestartCommand;
if (string.IsNullOrWhiteSpace(cmd?.Cmd))
{
return false;
}
Restart();
return true;
}
public bool RemoveStartupCommand(int index, out AutoCommand cmd)
{
using (var uow = _db.GetDbContext())
{
cmd = uow._context.AutoCommands
.AsNoTracking()
.Where(x => x.Interval == 0)
.Skip(index)
.FirstOrDefault();
if (cmd != null)
{
uow._context.Remove(cmd);
uow.SaveChanges();
return true;
}
}
return false;
}
public bool RemoveAutoCommand(int index, out AutoCommand cmd)
{
using (var uow = _db.GetDbContext())
{
cmd = uow._context.AutoCommands
.AsNoTracking()
.Where(x => x.Interval >= 5)
.Skip(index)
.FirstOrDefault();
if (cmd != null)
{
uow._context.Remove(cmd);
if (_autoCommands.TryGetValue(cmd.GuildId, out var autos))
if (autos.TryRemove(cmd.Id, out var timer))
timer.Change(Timeout.Infinite, Timeout.Infinite);
uow.SaveChanges();
return true;
}
}
return false;
}
public async Task<bool> SetAvatar(string img)
{
if (string.IsNullOrWhiteSpace(img))
return false;
if (!Uri.IsWellFormedUriString(img, UriKind.Absolute))
return false;
var uri = new Uri(img);
using (var http = _httpFactory.CreateClient())
using (var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (!sr.IsImage())
return false;
// i can't just do ReadAsStreamAsync because dicord.net's image poops itself
var imgData = await sr.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
using (var imgStream = imgData.ToStream())
{
await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)).ConfigureAwait(false);
}
}
return true;
}
public void ClearStartupCommands()
{
using (var uow = _db.GetDbContext())
{
var toRemove = uow._context
.AutoCommands
.AsNoTracking()
.Where(x => x.Interval == 0);
uow._context.AutoCommands.RemoveRange(toRemove);
uow.SaveChanges();
}
}
public void ReloadImages()
{
var sub = _cache.Redis.GetSubscriber();
sub.Publish(_creds.RedisKey() + "_reload_images", "");
}
public void Die()
{
var sub = _cache.Redis.GetSubscriber();
sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget);
}
public void Restart()
{
Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args);
var sub = _cache.Redis.GetSubscriber();
sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget);
}
public bool RestartShard(int shardId)
{
if (shardId < 0 || shardId >= _creds.TotalShards)
return false;
var pub = _cache.Redis.GetSubscriber();
pub.Publish(_creds.RedisKey() + "_shardcoord_stop",
JsonConvert.SerializeObject(shardId),
CommandFlags.FireAndForget);
return true;
}
public bool ForwardMessages()
{
var isForwarding = false;
_bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; });
return isForwarding;
}
public bool ForwardToAll()
{
var isToAll = false;
_bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; });
return isToAll;
}
public IEnumerable<ShardComMessage> GetAllShardStatuses()
{
var db = _cache.Redis.GetDatabase();
return db.ListRange(_creds.RedisKey() + "_shardstats")
.Select(x => JsonConvert.DeserializeObject<ShardComMessage>(x));
}
}
}

View File

@@ -0,0 +1,488 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Replacements;
using NadekoBot.Core.Common.TypeReaders.Models;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using NadekoBot.Modules.Permissions.Services;
using Newtonsoft.Json;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public class UserPunishService : INService
{
private readonly MuteService _mute;
private readonly DbService _db;
private readonly BlacklistService _blacklistService;
private readonly Timer _warnExpiryTimer;
public UserPunishService(MuteService mute, DbService db, BlacklistService blacklistService)
{
_mute = mute;
_db = db;
_blacklistService = blacklistService;
_warnExpiryTimer = new Timer(async _ =>
{
await CheckAllWarnExpiresAsync();
}, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(12));
}
public async Task<WarningPunishment> Warn(IGuild guild, ulong userId, IUser mod, string reason)
{
var modName = mod.ToString();
if (string.IsNullOrWhiteSpace(reason))
reason = "-";
var guildId = guild.Id;
var warn = new Warning()
{
UserId = userId,
GuildId = guildId,
Forgiven = false,
Reason = reason,
Moderator = modName,
};
int warnings = 1;
List<WarningPunishment> ps;
using (var uow = _db.GetDbContext())
{
ps = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.WarnPunishments))
.WarnPunishments;
warnings += uow.Warnings
.ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
.Count();
uow.Warnings.Add(warn);
uow.SaveChanges();
}
var p = ps.FirstOrDefault(x => x.Count == warnings);
if (p != null)
{
var user = await guild.GetUserAsync(userId).ConfigureAwait(false);
if (user == null)
return null;
await ApplyPunishment(guild, user, mod, p.Punishment, p.Time, p.RoleId, "Warned too many times.");
return p;
}
return null;
}
public async Task ApplyPunishment(IGuild guild, IGuildUser user, IUser mod, PunishmentAction p, int minutes,
ulong? roleId, string reason)
{
switch (p)
{
case PunishmentAction.Mute:
if (minutes == 0)
await _mute.MuteUser(user, mod, reason: reason).ConfigureAwait(false);
else
await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason)
.ConfigureAwait(false);
break;
case PunishmentAction.VoiceMute:
if (minutes == 0)
await _mute.MuteUser(user, mod, MuteType.Voice, reason).ConfigureAwait(false);
else
await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason)
.ConfigureAwait(false);
break;
case PunishmentAction.ChatMute:
if (minutes == 0)
await _mute.MuteUser(user, mod, MuteType.Chat, reason).ConfigureAwait(false);
else
await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason)
.ConfigureAwait(false);
break;
case PunishmentAction.Kick:
await user.KickAsync(reason).ConfigureAwait(false);
break;
case PunishmentAction.Ban:
if (minutes == 0)
await guild.AddBanAsync(user, reason: reason).ConfigureAwait(false);
else
await _mute.TimedBan(user.Guild, user, TimeSpan.FromMinutes(minutes), reason)
.ConfigureAwait(false);
break;
case PunishmentAction.Softban:
await guild.AddBanAsync(user, 7, reason: $"Softban | {reason}").ConfigureAwait(false);
try
{
await guild.RemoveBanAsync(user).ConfigureAwait(false);
}
catch
{
await guild.RemoveBanAsync(user).ConfigureAwait(false);
}
break;
case PunishmentAction.RemoveRoles:
await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole))
.ConfigureAwait(false);
break;
case PunishmentAction.AddRole:
if (roleId is null)
return;
var role = guild.GetRole(roleId.Value);
if (!(role is null))
{
if (minutes == 0)
await user.AddRoleAsync(role).ConfigureAwait(false);
else
await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role)
.ConfigureAwait(false);
}
else
{
Log.Warning($"Can't find role {roleId.Value} on server {guild.Id} to apply punishment.");
}
break;
default:
break;
}
}
public async Task CheckAllWarnExpiresAsync()
{
using (var uow = _db.GetDbContext())
{
var cleared = await uow._context.Database.ExecuteSqlRawAsync($@"UPDATE Warnings
SET Forgiven = 1,
ForgivenBy = 'Expiry'
WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND WarnExpireAction = 0)
AND Forgiven = 0
AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));");
var deleted = await uow._context.Database.ExecuteSqlRawAsync($@"DELETE FROM Warnings
WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND WarnExpireAction = 1)
AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));");
if(cleared > 0 || deleted > 0)
{
Log.Information($"Cleared {cleared} warnings and deleted {deleted} warnings due to expiry.");
}
}
}
public async Task CheckWarnExpiresAsync(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guildId, inc => inc);
if (config.WarnExpireHours == 0)
return;
var hours = $"{-config.WarnExpireHours} hours";
if (config.WarnExpireAction == WarnExpireAction.Clear)
{
await uow._context.Database.ExecuteSqlInterpolatedAsync($@"UPDATE warnings
SET Forgiven = 1,
ForgivenBy = 'Expiry'
WHERE GuildId={guildId}
AND Forgiven = 0
AND DateAdded < datetime('now', {hours})");
}
else if (config.WarnExpireAction == WarnExpireAction.Delete)
{
await uow._context.Database.ExecuteSqlInterpolatedAsync($@"DELETE FROM warnings
WHERE GuildId={guildId}
AND DateAdded < datetime('now', {hours})");
}
await uow.SaveChangesAsync();
}
}
public Task<int> GetWarnExpire(ulong guildId)
{
using var uow = _db.GetDbContext();
var config = uow.GuildConfigs.ForId(guildId);
return Task.FromResult(config.WarnExpireHours / 24);
}
public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
{
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(guildId, inc => inc);
config.WarnExpireHours = days * 24;
config.WarnExpireAction = delete ? WarnExpireAction.Delete : WarnExpireAction.Clear;
await uow.SaveChangesAsync();
// no need to check for warn expires
if (config.WarnExpireHours == 0)
return;
}
await CheckWarnExpiresAsync(guildId);
}
public IGrouping<ulong, Warning>[] WarnlogAll(ulong gid)
{
using (var uow = _db.GetDbContext())
{
return uow.Warnings.GetForGuild(gid).GroupBy(x => x.UserId).ToArray();
}
}
public Warning[] UserWarnings(ulong gid, ulong userId)
{
using (var uow = _db.GetDbContext())
{
return uow.Warnings.ForId(gid, userId);
}
}
public async Task<bool> WarnClearAsync(ulong guildId, ulong userId, int index, string moderator)
{
bool toReturn = true;
using (var uow = _db.GetDbContext())
{
if (index == 0)
{
await uow.Warnings.ForgiveAll(guildId, userId, moderator);
}
else
{
toReturn = uow.Warnings.Forgive(guildId, userId, moderator, index - 1);
}
uow.SaveChanges();
}
return toReturn;
}
public bool WarnPunish(ulong guildId, int number, PunishmentAction punish, StoopidTime time, IRole role = null)
{
// these 3 don't make sense with time
if ((punish == PunishmentAction.Softban || punish == PunishmentAction.Kick || punish == PunishmentAction.RemoveRoles) && time != null)
return false;
if (number <= 0 || (time != null && time.Time > TimeSpan.FromDays(49)))
return false;
using (var uow = _db.GetDbContext())
{
var ps = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
var toDelete = ps.Where(x => x.Count == number);
uow._context.RemoveRange(toDelete);
ps.Add(new WarningPunishment()
{
Count = number,
Punishment = punish,
Time = (int?)(time?.Time.TotalMinutes) ?? 0,
RoleId = punish == PunishmentAction.AddRole ? role.Id : default(ulong?),
});
uow.SaveChanges();
}
return true;
}
public bool WarnPunishRemove(ulong guildId, int number)
{
if (number <= 0)
return false;
using (var uow = _db.GetDbContext())
{
var ps = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
var p = ps.FirstOrDefault(x => x.Count == number);
if (p != null)
{
uow._context.Remove(p);
uow.SaveChanges();
}
}
return true;
}
public WarningPunishment[] WarnPunishList(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
return uow.GuildConfigs.ForId(guildId, gc => gc.Include(x => x.WarnPunishments))
.WarnPunishments
.OrderBy(x => x.Count)
.ToArray();
}
}
public (IEnumerable<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(SocketGuild guild, string people)
{
var gusers = guild.Users;
//get user objects and reasons
var bans = people.Split("\n")
.Select(x =>
{
var split = x.Trim().Split(" ");
var reason = string.Join(" ", split.Skip(1));
if (ulong.TryParse(split[0], out var id))
return (Original: split[0], Id: id, Reason: reason);
return (Original: split[0],
Id: gusers
.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)
?.Id,
Reason: reason);
})
.ToArray();
//if user is null, means that person couldn't be found
var missing = bans
.Count(x => !x.Id.HasValue);
//get only data for found users
var found = bans
.Where(x => x.Id.HasValue)
.Select(x => x.Id.Value)
.ToList();
_blacklistService.BlacklistUsers(found);
return (bans, missing);
}
public string GetBanTemplate(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
var template = uow._context.BanTemplates
.AsQueryable()
.FirstOrDefault(x => x.GuildId == guildId);
return template?.Text;
}
}
public void SetBanTemplate(ulong guildId, string text)
{
using (var uow = _db.GetDbContext())
{
var template = uow._context.BanTemplates
.AsQueryable()
.FirstOrDefault(x => x.GuildId == guildId);
if (text == null)
{
if (template is null)
return;
uow._context.Remove(template);
}
else if (template == null)
{
uow._context.BanTemplates.Add(new BanTemplate()
{
GuildId = guildId,
Text = text,
});
}
else
{
template.Text = text;
}
uow.SaveChanges();
}
}
public CREmbed GetBanUserDmEmbed(ICommandContext context, IGuildUser target, string defaultMessage,
string banReason, TimeSpan? duration)
{
return GetBanUserDmEmbed(
(DiscordSocketClient) context.Client,
(SocketGuild) context.Guild,
(IGuildUser) context.User,
target,
defaultMessage,
banReason,
duration);
}
public CREmbed GetBanUserDmEmbed(DiscordSocketClient client, SocketGuild guild,
IGuildUser moderator, IGuildUser target, string defaultMessage, string banReason, TimeSpan? duration)
{
var template = GetBanTemplate(guild.Id);
banReason = string.IsNullOrWhiteSpace(banReason)
? "-"
: banReason;
var replacer = new ReplacementBuilder()
.WithServer(client, guild)
.WithOverride("%ban.mod%", () => moderator.ToString())
.WithOverride("%ban.mod.fullname%", () => moderator.ToString())
.WithOverride("%ban.mod.name%", () => moderator.Username)
.WithOverride("%ban.mod.discrim%", () => moderator.Discriminator)
.WithOverride("%ban.user%", () => target.ToString())
.WithOverride("%ban.user.fullname%", () => target.ToString())
.WithOverride("%ban.user.name%", () => target.Username)
.WithOverride("%ban.user.discrim%", () => target.Discriminator)
.WithOverride("%reason%", () => banReason)
.WithOverride("%ban.reason%", () => banReason)
.WithOverride("%ban.duration%", () => duration?.ToString(@"d\.hh\:mm")?? "perma")
.Build();
CREmbed crEmbed = null;
// if template isn't set, use the old message style
if (string.IsNullOrWhiteSpace(template))
{
template = JsonConvert.SerializeObject(new
{
color = NadekoBot.ErrorColor.RawValue,
description = defaultMessage
});
CREmbed.TryParse(template, out crEmbed);
}
// if template is set to "-" do not dm the user
else if (template == "-")
{
return default;
}
// if template is an embed, send that embed with replacements
else if (CREmbed.TryParse(template, out crEmbed))
{
replacer.Replace(crEmbed);
}
// otherwise, treat template as a regular string with replacements
else
{
template = JsonConvert.SerializeObject(new
{
color = NadekoBot.ErrorColor.RawValue,
description = replacer.Replace(template)
});
CREmbed.TryParse(template, out crEmbed);
}
return crEmbed;
}
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public class VcRoleService : INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>> VcRoles { get; }
public ConcurrentDictionary<ulong, ConcurrentQueue<(bool, IGuildUser, IRole)>> ToAssign { get; }
public VcRoleService(DiscordSocketClient client, NadekoBot bot, DbService db)
{
_db = db;
_client = client;
_client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated;
VcRoles = new ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>>();
ToAssign = new ConcurrentDictionary<ulong, ConcurrentQueue<(bool, IGuildUser, IRole)>>();
var missingRoles = new ConcurrentBag<VcRoleInfo>();
using (var uow = db.GetDbContext())
{
var guildIds = client.Guilds.Select(x => x.Id).ToList();
var configs = uow._context.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.VcRoleInfos)
.Where(x => guildIds.Contains(x.GuildId))
.ToList();
Task.WhenAll(configs.Select(InitializeVcRole));
}
Task.Run(async () =>
{
while (true)
{
var tasks = ToAssign.Values.Select(queue => Task.Run(async () =>
{
while (queue.TryDequeue(out var item))
{
var (add, user, role) = item;
if (add)
{
if (!user.RoleIds.Contains(role.Id))
{
try { await user.AddRoleAsync(role).ConfigureAwait(false); } catch { }
}
}
else
{
if (user.RoleIds.Contains(role.Id))
{
try { await user.RemoveRoleAsync(role).ConfigureAwait(false); } catch { }
}
}
await Task.Delay(250).ConfigureAwait(false);
}
}));
await Task.WhenAll(tasks.Append(Task.Delay(1000))).ConfigureAwait(false);
}
});
_client.LeftGuild += _client_LeftGuild;
bot.JoinedGuild += Bot_JoinedGuild;
}
private Task Bot_JoinedGuild(GuildConfig arg)
{
// includeall no longer loads vcrole
// need to load new guildconfig with vc role included
using (var uow = _db.GetDbContext())
{
var configWithVcRole = uow.GuildConfigs.ForId(
arg.GuildId,
set => set.Include(x => x.VcRoleInfos)
);
var _ = InitializeVcRole(configWithVcRole);
}
return Task.CompletedTask;
}
private Task _client_LeftGuild(SocketGuild arg)
{
VcRoles.TryRemove(arg.Id, out _);
ToAssign.TryRemove(arg.Id, out _);
return Task.CompletedTask;
}
private async Task InitializeVcRole(GuildConfig gconf)
{
await Task.Yield();
var g = _client.GetGuild(gconf.GuildId);
if (g == null)
return;
var infos = new ConcurrentDictionary<ulong, IRole>();
var missingRoles = new List<VcRoleInfo>();
VcRoles.AddOrUpdate(gconf.GuildId, infos, delegate { return infos; });
foreach (var ri in gconf.VcRoleInfos)
{
var role = g.GetRole(ri.RoleId);
if (role == null)
{
missingRoles.Add(ri);
continue;
}
infos.TryAdd(ri.VoiceChannelId, role);
}
if (missingRoles.Any())
{
using (var uow = _db.GetDbContext())
{
Log.Warning($"Removing {missingRoles.Count} missing roles from {nameof(VcRoleService)}");
uow._context.RemoveRange(missingRoles);
await uow.SaveChangesAsync();
}
}
}
public void AddVcRole(ulong guildId, IRole role, ulong vcId)
{
if (role == null)
throw new ArgumentNullException(nameof(role));
var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary<ulong, IRole>());
guildVcRoles.AddOrUpdate(vcId, role, (key, old) => role);
using (var uow = _db.GetDbContext())
{
var conf = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.VcRoleInfos));
var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one
if(toDelete != null)
{
uow._context.Remove(toDelete);
}
conf.VcRoleInfos.Add(new VcRoleInfo()
{
VoiceChannelId = vcId,
RoleId = role.Id,
}); // add new one
uow.SaveChanges();
}
}
public bool RemoveVcRole(ulong guildId, ulong vcId)
{
if (!VcRoles.TryGetValue(guildId, out var guildVcRoles))
return false;
if (!guildVcRoles.TryRemove(vcId, out _))
return false;
using (var uow = _db.GetDbContext())
{
var conf = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.VcRoleInfos));
var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList();
uow._context.RemoveRange(toRemove);
uow.SaveChanges();
}
return true;
}
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(() =>
{
try
{
if (oldVc != newVc)
{
ulong guildId;
guildId = newVc?.Guild.Id ?? oldVc.Guild.Id;
if (VcRoles.TryGetValue(guildId, out ConcurrentDictionary<ulong, IRole> guildVcRoles))
{
//remove old
if (oldVc != null && guildVcRoles.TryGetValue(oldVc.Id, out IRole role))
{
Assign(false, gusr, role);
}
//add new
if (newVc != null && guildVcRoles.TryGetValue(newVc.Id, out role))
{
Assign(true, gusr, role);
}
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error in VcRoleService VoiceStateUpdate");
}
});
return Task.CompletedTask;
}
private void Assign(bool v, SocketGuildUser gusr, IRole role)
{
var queue = ToAssign.GetOrAdd(gusr.Guild.Id, new ConcurrentQueue<(bool, IGuildUser, IRole)>());
queue.Enqueue((v, gusr, role));
}
}
}