Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BOAT] Make manual mutes count towards rule -1 #75

Open
wants to merge 2 commits into
base: my-awesome-branch
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/boat/src/commands/mod/mute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,10 @@ export function executor (msg: Message<GuildTextableChannel>, args: string[]): v

mute(msg.channel.guild, target, msg.author, reason, duration)
msg.channel.createMessage('Shut')
msg._client.mongo.collection('enforce').insertOne({
userId: target,
modId: msg.author.id,
rule: -1,
})
return
}
36 changes: 5 additions & 31 deletions packages/boat/src/modules/mod/modlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,16 @@
* SOFTWARE.
*/

import type { CommandClient, Guild, GuildAuditLogEntry, Role, TextableChannel, User } from 'eris'
import type { CommandClient, Guild, Role, TextableChannel, User } from 'eris'
import { Constants } from 'eris'
import { sanitizeMarkdown } from '../../util.js'
import { delayedFunction, extractEntryData, sanitizeMarkdown } from '../../util.js'
import config from '../../config.js'

const TEMPLATE = `**$type | Case $case**
__User__: $user ($userid)
__Moderator__: $moderator ($modid)
__Reason__: $reason`

function delayedFunction (fn: Function): () => void {
return function (this: CommandClient, ...args: unknown[]) {
setTimeout(() => fn.apply(this, args), 2e3)
}
}

function format (type: string, caseId: string, user: User, modName: string, modId: string, reason: string) {
return TEMPLATE
Expand All @@ -58,27 +53,6 @@ async function computeCaseId (channel: TextableChannel): Promise<number> {
return parseInt(match[1], 10) + 1
}

function extractEntryData (entry: GuildAuditLogEntry): [ string, string, string ] {
let modId = ''
let modName = ''
let reason = ''

if (entry.user.id === config.discord.clientID && entry.reason?.startsWith('[')) {
const splittedReason = entry.reason.split(' ')
modName = splittedReason.shift()!.replace('[', '').replace(']', '')
reason = splittedReason.join(' ')
const [ username, discrim ] = modName.split('#')
const mod = entry.guild.members.find((m) => m.username === username && m.discriminator === discrim)
modId = mod ? mod.id : '<unknown>' // Should not happen
} else {
modId = entry.user.id
modName = `${entry.user.username}#${entry.user.discriminator}`
reason = entry.reason || 'No reason specified.'
}

return [ modId, modName, reason ]
}

function processBanFactory (type: 'add' | 'remove'): (guild: Guild, user: User) => Promise<void> {
return async function (this: CommandClient, guild: Guild, user: User): Promise<void> {
const channel = this.getChannel(config.discord.ids.channelModLogs)
Expand All @@ -91,7 +65,7 @@ function processBanFactory (type: 'add' | 'remove'): (guild: Guild, user: User)
const entry = logs.entries.find((auditEntry) => auditEntry.targetID === user.id)
if (!entry) return

let [ modId, modName, reason ] = extractEntryData(entry)
let { modId, modName, reason } = extractEntryData(entry)

const soft = reason.startsWith('[soft]')
if (soft) {
Expand All @@ -116,7 +90,7 @@ async function processMemberLeave (this: CommandClient, guild: Guild, user: User
const logs = await guild.getAuditLog({ actionType: Constants.AuditLogActions.MEMBER_KICK, limit: 5 })
const entry = logs.entries.find((auditEntry) => auditEntry.targetID === user.id)
if (entry && Date.now() - Number((BigInt(entry.id) >> BigInt('22')) + BigInt('1420070400000')) < 5000) {
const [ modId, modName, reason ] = extractEntryData(entry)
const { modId, modName, reason } = extractEntryData(entry)
const caseId = await computeCaseId(channel)

this.createMessage(config.discord.ids.channelModLogs, {
Expand Down Expand Up @@ -147,7 +121,7 @@ async function processMemberUpdate (this: CommandClient, guild: Guild, user: Use
continue
}

const [ modId, modName, reason ] = extractEntryData(entry)
const { modId, modName, reason } = extractEntryData(entry)
const caseId = await computeCaseId(channel)

this.createMessage(config.discord.ids.channelModLogs, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
* SOFTWARE.
*/

import type { CommandClient, Guild, Member, MemberPartial } from 'eris'
import { CommandClient, Constants, Guild, Member, MemberPartial, Role, User } from 'eris'
import { ban } from '../../mod.js'
import { delayedFunction, extractEntryData } from '../../util.js'
import config from '../../config.js'

const MAX_INFRACTIONS = 4
Expand All @@ -44,6 +45,47 @@ async function memberRemove (this: CommandClient, guild: Guild, member: Member |
}
}

async function processMemberUpdate (this: CommandClient, guild: Guild, user: User) {
const channel = this.getChannel(config.discord.ids.channelModLogs)
if (!channel || !('getMessages' in channel)) return

const logs = await guild.getAuditLog({ actionType: Constants.AuditLogActions.MEMBER_ROLE_UPDATE, limit: 5 })

for (const entry of logs.entries) {
if (entry.targetID !== user.id
|| entry.user.id === config.discord.clientID
|| !entry.after
|| Date.now() - Number((BigInt(entry.id) >> BigInt('22')) + BigInt('1420070400000')) > 5000) {
continue
}

const added = entry.after.$add as Role[] | null
const removed = entry.after.$remove as Role[] | null

const wasAdded = Boolean(added?.find((r) => r.id === config.discord.ids.roleMuted))
const wasRemoved = Boolean(removed?.find((r) => r.id === config.discord.ids.roleMuted))

if (wasRemoved || wasAdded === wasRemoved) {
continue
}

const { modId } = extractEntryData(entry)

if (modId === config.discord.clientID) {
continue
}

this.mongo.collection('enforce').insertOne({
userId: user.id,
modId: modId,
rule: -1,
})

break
}
}

export default function (bot: CommandClient) {
bot.on('guildMemberRemove', memberRemove)
bot.on('guildMemberUpdate', delayedFunction(processMemberUpdate))
}
44 changes: 43 additions & 1 deletion packages/boat/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
* SOFTWARE.
*/

import type { Guild, GuildTextableChannel, Member, Message } from 'eris'
import type { CommandClient, Guild, GuildAuditLogEntry, GuildTextableChannel, Member, Message } from 'eris'
import { readdir, stat } from 'fs/promises'
import { URL } from 'url'
import config from './config.js'

const DURATION_MAP = { m: 60e3, h: 3600e3, d: 86400e3 }
const BYTE_UNITS = [ 'B', 'KB', 'MB', 'GB', 'TB' ]
Expand Down Expand Up @@ -168,3 +169,44 @@ export function prettyPrintBytes (bytes: number): string {

return `${bytes.toFixed(2)} ${BYTE_UNITS[unitIdx]}`
}

/**
* Data pertaining to an Audit Log Entry.
*/
export type AuditEntryData = {
modId: string,
modName: string,
reason: string;
}

/**
* Extract data from a GuildAuditLogEntry.
* @param entry The GuildAuditLogEntry from which to extract the data
* @returns A `AuditEntryData` containing data from `entry`
*/
export function extractEntryData (entry: GuildAuditLogEntry): AuditEntryData {
let modId = ''
let modName = ''
let reason = ''

if (entry.user.id === config.discord.clientID && entry.reason?.startsWith('[')) {
const splittedReason = entry.reason.split(' ')
modName = splittedReason.shift()!.replace('[', '').replace(']', '')
reason = splittedReason.join(' ')
const [ username, discrim ] = modName.split('#')
const mod = entry.guild.members.find((m) => m.username === username && m.discriminator === discrim)
modId = mod ? mod.id : '<unknown>' // Should not happen
} else {
modId = entry.user.id
modName = `${entry.user.username}#${entry.user.discriminator}`
reason = entry.reason || 'No reason specified.'
}

return { modId: modId, modName: modName, reason: reason }
}

export function delayedFunction (fn: Function): () => void {
return function (this: CommandClient, ...args: unknown[]) {
setTimeout(() => fn.apply(this, args), 2e3)
}
}