2024-06-18 23:50:02 +12:00
#nullable disable
using LinqToDB ;
using LinqToDB.EntityFrameworkCore ;
using Microsoft.EntityFrameworkCore ;
using EllieBot.Common.ModuleBehaviors ;
using EllieBot.Common.TypeReaders.Models ;
using EllieBot.Modules.Permissions.Services ;
using EllieBot.Db.Models ;
using Newtonsoft.Json ;
namespace EllieBot.Modules.Administration.Services ;
public class UserPunishService : IEService , IReadyExecutor
{
private readonly MuteService _mute ;
private readonly DbService _db ;
private readonly BlacklistService _blacklistService ;
private readonly BotConfigService _bcs ;
private readonly DiscordSocketClient _client ;
private readonly IReplacementService _repSvc ;
public event Func < Warning , Task > OnUserWarned = static delegate { return Task . CompletedTask ; } ;
public UserPunishService (
MuteService mute ,
DbService db ,
BlacklistService blacklistService ,
BotConfigService bcs ,
DiscordSocketClient client ,
IReplacementService repSvc )
{
_mute = mute ;
_db = db ;
_blacklistService = blacklistService ;
_bcs = bcs ;
_client = client ;
_repSvc = repSvc ;
}
public async Task OnReadyAsync ( )
{
if ( _client . ShardId ! = 0 )
return ;
using var expiryTimer = new PeriodicTimer ( TimeSpan . FromHours ( 12 ) ) ;
do
{
try
{
await CheckAllWarnExpiresAsync ( ) ;
}
catch ( Exception ex )
{
Log . Error ( ex , "Unexpected error while checking for warn expiries: {ErrorMessage}" , ex . Message ) ;
}
} while ( await expiryTimer . WaitForNextTickAsync ( ) ) ;
}
public async Task < WarningPunishment > Warn (
IGuild guild ,
ulong userId ,
IUser mod ,
long weight ,
string reason )
{
ArgumentOutOfRangeException . ThrowIfNegativeOrZero ( weight ) ;
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 ,
Weight = weight
} ;
long previousCount ;
List < WarningPunishment > ps ;
await using ( var uow = _db . GetDbContext ( ) )
{
ps = uow . GuildConfigsForId ( guildId , set = > set . Include ( x = > x . WarnPunishments ) ) . WarnPunishments ;
2024-07-27 19:13:39 +12:00
previousCount = uow . Set < Warning > ( )
. ForId ( guildId , userId )
. Where ( w = > ! w . Forgiven & & w . UserId = = userId )
. Sum ( x = > x . Weight ) ;
2024-06-18 23:50:02 +12:00
uow . Set < Warning > ( ) . Add ( warn ) ;
await uow . SaveChangesAsync ( ) ;
}
_ = OnUserWarned ( warn ) ;
var totalCount = previousCount + weight ;
var p = ps . Where ( x = > x . Count > previousCount & & x . Count < = totalCount )
2024-07-27 19:13:39 +12:00
. MaxBy ( x = > x . Count ) ;
2024-06-18 23:50:02 +12:00
if ( p is not null )
{
var user = await guild . GetUserAsync ( userId ) ;
if ( user is 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 )
{
if ( ! await CheckPermission ( guild , p ) )
return ;
int banPrune ;
switch ( p )
{
case PunishmentAction . Mute :
if ( minutes = = 0 )
await _mute . MuteUser ( user , mod , reason : reason ) ;
else
await _mute . TimedMute ( user , mod , TimeSpan . FromMinutes ( minutes ) , reason : reason ) ;
break ;
case PunishmentAction . VoiceMute :
if ( minutes = = 0 )
await _mute . MuteUser ( user , mod , MuteType . Voice , reason ) ;
else
await _mute . TimedMute ( user , mod , TimeSpan . FromMinutes ( minutes ) , MuteType . Voice , reason ) ;
break ;
case PunishmentAction . ChatMute :
if ( minutes = = 0 )
await _mute . MuteUser ( user , mod , MuteType . Chat , reason ) ;
else
await _mute . TimedMute ( user , mod , TimeSpan . FromMinutes ( minutes ) , MuteType . Chat , reason ) ;
break ;
case PunishmentAction . Kick :
await user . KickAsync ( reason ) ;
break ;
case PunishmentAction . Ban :
banPrune = await GetBanPruneAsync ( user . GuildId ) ? ? 7 ;
if ( minutes = = 0 )
await guild . AddBanAsync ( user , reason : reason , pruneDays : banPrune ) ;
else
await _mute . TimedBan ( user . Guild , user . Id , TimeSpan . FromMinutes ( minutes ) , reason , banPrune ) ;
break ;
case PunishmentAction . Softban :
banPrune = await GetBanPruneAsync ( user . GuildId ) ? ? 7 ;
await guild . AddBanAsync ( user , banPrune , $"Softban | {reason}" ) ;
try
{
await guild . RemoveBanAsync ( user ) ;
}
catch
{
await guild . RemoveBanAsync ( user ) ;
}
break ;
case PunishmentAction . RemoveRoles :
await user . RemoveRolesAsync ( user . GetRoles ( ) . Where ( x = > ! x . IsManaged & & x ! = x . Guild . EveryoneRole ) ) ;
break ;
case PunishmentAction . AddRole :
if ( roleId is null )
return ;
var role = guild . GetRole ( roleId . Value ) ;
if ( role is not null )
{
if ( minutes = = 0 )
await user . AddRoleAsync ( role ) ;
else
await _mute . TimedRole ( user , TimeSpan . FromMinutes ( minutes ) , reason , role ) ;
}
else
{
Log . Warning ( "Can't find role {RoleId} on server {GuildId} to apply punishment" ,
roleId . Value ,
guild . Id ) ;
}
break ;
case PunishmentAction . Warn :
await Warn ( guild , user . Id , mod , 1 , reason ) ;
break ;
case PunishmentAction . TimeOut :
await user . SetTimeOutAsync ( TimeSpan . FromMinutes ( minutes ) ) ;
break ;
}
}
/// <summary>
/// Used to prevent the bot from hitting 403's when it needs to
/// apply punishments with insufficient permissions
/// </summary>
/// <param name="guild">Guild the punishment is applied in</param>
/// <param name="punish">Punishment to apply</param>
/// <returns>Whether the bot has sufficient permissions</returns>
private async Task < bool > CheckPermission ( IGuild guild , PunishmentAction punish )
{
var botUser = await guild . GetCurrentUserAsync ( ) ;
switch ( punish )
{
case PunishmentAction . Mute :
return botUser . GuildPermissions . MuteMembers & & botUser . GuildPermissions . ManageRoles ;
case PunishmentAction . Kick :
return botUser . GuildPermissions . KickMembers ;
case PunishmentAction . Ban :
return botUser . GuildPermissions . BanMembers ;
case PunishmentAction . Softban :
return botUser . GuildPermissions . BanMembers ; // ban + unban
case PunishmentAction . RemoveRoles :
return botUser . GuildPermissions . ManageRoles ;
case PunishmentAction . ChatMute :
2024-07-08 01:34:34 +12:00
return botUser . GuildPermissions . ManageRoles ; // adds ellie-mute role
2024-06-18 23:50:02 +12:00
case PunishmentAction . VoiceMute :
return botUser . GuildPermissions . MuteMembers ;
case PunishmentAction . AddRole :
return botUser . GuildPermissions . ManageRoles ;
case PunishmentAction . TimeOut :
return botUser . GuildPermissions . ModerateMembers ;
default :
return true ;
}
}
public async Task CheckAllWarnExpiresAsync ( )
{
await using var uow = _db . GetDbContext ( ) ;
2024-09-12 15:44:35 +12:00
2024-09-20 19:15:37 +12:00
var toClear = await uow . GetTable < Warning > ( )
2024-09-12 15:44:35 +12:00
. Where ( x = > uow . GetTable < GuildConfig > ( )
. Count ( y = > y . GuildId = = x . GuildId
& & y . WarnExpireHours > 0
& & y . WarnExpireAction = = WarnExpireAction . Clear )
> 0
2024-07-27 19:13:39 +12:00
& & x . Forgiven = = false
& & x . DateAdded
2024-09-12 15:44:35 +12:00
< DateTime . UtcNow . AddHours ( - uow . GetTable < GuildConfig > ( )
2024-07-27 19:13:39 +12:00
. Where ( y = > x . GuildId = = y . GuildId )
. Select ( y = > y . WarnExpireHours )
. First ( ) ) )
2024-09-20 19:15:37 +12:00
. Select ( x = > x . Id )
. ToListAsyncLinqToDB ( ) ;
var cleared = await uow . GetTable < Warning > ( )
. Where ( x = > toClear . Contains ( x . Id ) )
. UpdateAsync ( _ = > new ( )
{
Forgiven = true ,
ForgivenBy = "expiry"
} ) ;
var toDelete = await uow . GetTable < Warning > ( )
. Where ( x = > uow . GetTable < GuildConfig > ( )
. Count ( y = > y . GuildId = = x . GuildId
& & y . WarnExpireHours > 0
& & y . WarnExpireAction = = WarnExpireAction . Delete )
> 0
& & x . DateAdded
< DateTime . UtcNow . AddHours ( - uow . GetTable < GuildConfig > ( )
. Where ( y = > x . GuildId = = y . GuildId )
. Select ( y = > y . WarnExpireHours )
. First ( ) ) )
. Select ( x = > x . Id )
. ToListAsyncLinqToDB ( ) ;
2024-06-18 23:50:02 +12:00
2024-09-12 15:44:35 +12:00
var deleted = await uow . GetTable < Warning > ( )
2024-09-20 19:15:37 +12:00
. Where ( x = > toDelete . Contains ( x . Id ) )
. DeleteAsync ( ) ;
2024-06-18 23:50:02 +12:00
if ( cleared > 0 | | deleted > 0 )
{
Log . Information ( "Cleared {ClearedWarnings} warnings and deleted {DeletedWarnings} warnings due to expiry" ,
cleared ,
2024-09-20 19:15:37 +12:00
toDelete . Count ) ;
2024-06-18 23:50:02 +12:00
}
}
public async Task CheckWarnExpiresAsync ( ulong guildId )
{
await using var uow = _db . GetDbContext ( ) ;
var config = uow . GuildConfigsForId ( guildId , inc = > inc ) ;
if ( config . WarnExpireHours = = 0 )
return ;
if ( config . WarnExpireAction = = WarnExpireAction . Clear )
{
await uow . Set < Warning > ( )
2024-07-27 19:13:39 +12:00
. Where ( x = > x . GuildId = = guildId
& & x . Forgiven = = false
& & x . DateAdded < DateTime . UtcNow . AddHours ( - config . WarnExpireHours ) )
. UpdateAsync ( _ = > new ( )
{
Forgiven = true ,
ForgivenBy = "expiry"
} ) ;
2024-06-18 23:50:02 +12:00
}
else if ( config . WarnExpireAction = = WarnExpireAction . Delete )
{
await uow . Set < Warning > ( )
2024-07-27 19:13:39 +12:00
. Where ( x = > x . GuildId = = guildId
& & x . DateAdded < DateTime . UtcNow . AddHours ( - config . WarnExpireHours ) )
. DeleteAsync ( ) ;
2024-06-18 23:50:02 +12:00
}
await uow . SaveChangesAsync ( ) ;
}
public Task < int > GetWarnExpire ( ulong guildId )
{
using var uow = _db . GetDbContext ( ) ;
var config = uow . GuildConfigsForId ( guildId , set = > set ) ;
return Task . FromResult ( config . WarnExpireHours / 24 ) ;
}
public async Task WarnExpireAsync ( ulong guildId , int days , bool delete )
{
await using ( var uow = _db . GetDbContext ( ) )
{
var config = uow . GuildConfigsForId ( 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 . Set < Warning > ( ) . GetForGuild ( gid ) . GroupBy ( x = > x . UserId ) . ToArray ( ) ;
}
public Warning [ ] UserWarnings ( ulong gid , ulong userId )
{
using var uow = _db . GetDbContext ( ) ;
return uow . Set < Warning > ( ) . ForId ( gid , userId ) ;
}
public async Task < bool > WarnClearAsync (
ulong guildId ,
ulong userId ,
int index ,
string moderator )
{
var toReturn = true ;
await using var uow = _db . GetDbContext ( ) ;
if ( index = = 0 )
await uow . Set < Warning > ( ) . ForgiveAll ( guildId , userId , moderator ) ;
else
toReturn = uow . Set < Warning > ( ) . Forgive ( guildId , userId , moderator , index - 1 ) ;
await uow . SaveChangesAsync ( ) ;
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 is PunishmentAction . Softban or PunishmentAction . Kick or PunishmentAction . RemoveRoles
& & time is not null )
return false ;
if ( number < = 0 | | ( time is not null & & time . Time > TimeSpan . FromDays ( 49 ) ) )
return false ;
if ( punish is PunishmentAction . AddRole & & role is null )
return false ;
if ( punish is PunishmentAction . TimeOut & & time is null )
return false ;
using var uow = _db . GetDbContext ( ) ;
var ps = uow . GuildConfigsForId ( guildId , set = > set . Include ( x = > x . WarnPunishments ) ) . WarnPunishments ;
var toDelete = ps . Where ( x = > x . Count = = number ) ;
uow . RemoveRange ( toDelete ) ;
ps . Add ( new ( )
{
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 . GuildConfigsForId ( guildId , set = > set . Include ( x = > x . WarnPunishments ) ) . WarnPunishments ;
var p = ps . FirstOrDefault ( x = > x . Count = = number ) ;
if ( p is not null )
{
uow . Remove ( p ) ;
uow . SaveChanges ( ) ;
}
return true ;
}
public WarningPunishment [ ] WarnPunishList ( ulong guildId )
{
using var uow = _db . GetDbContext ( ) ;
return uow . GuildConfigsForId ( guildId , gc = > gc . Include ( x = > x . WarnPunishments ) )
2024-07-27 19:13:39 +12:00
. WarnPunishments . OrderBy ( x = > x . Count )
. ToArray ( ) ;
2024-06-18 23:50:02 +12:00
}
public ( IReadOnlyCollection < ( 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" )
2024-07-27 19:13:39 +12:00
. Select ( x = >
{
var split = x . Trim ( ) . Split ( " " ) ;
2024-06-18 23:50:02 +12:00
2024-07-27 19:13:39 +12:00
var reason = string . Join ( " " , split . Skip ( 1 ) ) ;
2024-06-18 23:50:02 +12:00
2024-07-27 19:13:39 +12:00
if ( ulong . TryParse ( split [ 0 ] , out var id ) )
return ( Original : split [ 0 ] , Id : id , Reason : reason ) ;
2024-06-18 23:50:02 +12:00
2024-07-27 19:13:39 +12:00
return ( Original : split [ 0 ] ,
gusers . FirstOrDefault ( u = > u . ToString ( ) . ToLowerInvariant ( ) = = x ) ? . Id ,
Reason : reason ) ;
} )
. ToArray ( ) ;
2024-06-18 23:50:02 +12:00
//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 . Set < BanTemplate > ( ) . AsQueryable ( ) . FirstOrDefault ( x = > x . GuildId = = guildId ) ;
return template ? . Text ;
}
public void SetBanTemplate ( ulong guildId , string text )
{
using var uow = _db . GetDbContext ( ) ;
var template = uow . Set < BanTemplate > ( ) . AsQueryable ( ) . FirstOrDefault ( x = > x . GuildId = = guildId ) ;
if ( text is null )
{
if ( template is null )
return ;
uow . Remove ( template ) ;
}
else if ( template is null )
{
2024-07-27 19:13:39 +12:00
uow . Set < BanTemplate > ( )
. Add ( new ( )
{
GuildId = guildId ,
Text = text
} ) ;
2024-06-18 23:50:02 +12:00
}
else
template . Text = text ;
uow . SaveChanges ( ) ;
}
public async Task SetBanPruneAsync ( ulong guildId , int? pruneDays )
{
await using var ctx = _db . GetDbContext ( ) ;
await ctx . Set < BanTemplate > ( )
2024-07-27 19:13:39 +12:00
. ToLinqToDBTable ( )
. InsertOrUpdateAsync ( ( ) = > new ( )
{
GuildId = guildId ,
Text = null ,
DateAdded = DateTime . UtcNow ,
PruneDays = pruneDays
} ,
old = > new ( )
{
PruneDays = pruneDays
} ,
( ) = > new ( )
{
GuildId = guildId
} ) ;
2024-06-18 23:50:02 +12:00
}
public async Task < int? > GetBanPruneAsync ( ulong guildId )
{
await using var ctx = _db . GetDbContext ( ) ;
return await ctx . Set < BanTemplate > ( )
2024-07-27 19:13:39 +12:00
. Where ( x = > x . GuildId = = guildId )
. Select ( x = > x . PruneDays )
. FirstOrDefaultAsyncLinqToDB ( ) ;
2024-06-18 23:50:02 +12:00
}
public Task < SmartText > GetBanUserDmEmbed (
ICommandContext context ,
IGuildUser target ,
string defaultMessage ,
string banReason ,
TimeSpan ? duration )
= > GetBanUserDmEmbed ( ( DiscordSocketClient ) context . Client ,
( SocketGuild ) context . Guild ,
( IGuildUser ) context . User ,
target ,
defaultMessage ,
banReason ,
duration ) ;
public async Task < SmartText > 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 repCtx = new ReplacementContext ( client , guild )
2024-07-27 19:13:39 +12:00
. 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" ) ;
2024-06-18 23:50:02 +12:00
// if template isn't set, use the old message style
if ( string . IsNullOrWhiteSpace ( template ) )
{
template = JsonConvert . SerializeObject ( new
{
color = _bcs . Data . Color . Error . PackedValue > > 8 ,
description = defaultMessage
} ) ;
}
// 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
// otherwise, treat template as a regular string with replacements
else if ( SmartText . CreateFrom ( template ) is not { IsEmbed : true } or { IsEmbedArray : true } )
{
template = JsonConvert . SerializeObject ( new
{
color = _bcs . Data . Color . Error . PackedValue > > 8 ,
description = template
} ) ;
}
var output = SmartText . CreateFrom ( template ) ;
return await _repSvc . ReplaceAsync ( output , repCtx ) ;
}
2024-07-27 19:13:39 +12:00
public async Task < Warning > WarnDelete ( ulong userId , int index )
{
await using var uow = _db . GetDbContext ( ) ;
var warn = await uow . GetTable < Warning > ( )
. Where ( x = > x . UserId = = userId )
. OrderByDescending ( x = > x . DateAdded )
. Skip ( index )
. FirstOrDefaultAsyncLinqToDB ( ) ;
if ( warn is not null )
{
await uow . GetTable < Warning > ( )
. Where ( x = > x . Id = = warn . Id )
. DeleteAsync ( ) ;
}
return warn ;
}
2024-06-18 23:50:02 +12:00
}