GuildAuditLogs.js (13689B)
1 'use strict'; 2 3 const Integration = require('./Integration'); 4 const Webhook = require('./Webhook'); 5 const Collection = require('../util/Collection'); 6 const { PartialTypes } = require('../util/Constants'); 7 const Snowflake = require('../util/Snowflake'); 8 const Util = require('../util/Util'); 9 10 /** 11 * The target type of an entry, e.g. `GUILD`. Here are the available types: 12 * * GUILD 13 * * CHANNEL 14 * * USER 15 * * ROLE 16 * * INVITE 17 * * WEBHOOK 18 * * EMOJI 19 * * MESSAGE 20 * * INTEGRATION 21 * @typedef {string} AuditLogTargetType 22 */ 23 24 /** 25 * Key mirror of all available audit log targets. 26 * @name GuildAuditLogs.Targets 27 * @type {AuditLogTargetType} 28 */ 29 const Targets = { 30 ALL: 'ALL', 31 GUILD: 'GUILD', 32 CHANNEL: 'CHANNEL', 33 USER: 'USER', 34 ROLE: 'ROLE', 35 INVITE: 'INVITE', 36 WEBHOOK: 'WEBHOOK', 37 EMOJI: 'EMOJI', 38 MESSAGE: 'MESSAGE', 39 INTEGRATION: 'INTEGRATION', 40 UNKNOWN: 'UNKNOWN', 41 }; 42 43 /** 44 * The action of an entry. Here are the available actions: 45 * * ALL: null 46 * * GUILD_UPDATE: 1 47 * * CHANNEL_CREATE: 10 48 * * CHANNEL_UPDATE: 11 49 * * CHANNEL_DELETE: 12 50 * * CHANNEL_OVERWRITE_CREATE: 13 51 * * CHANNEL_OVERWRITE_UPDATE: 14 52 * * CHANNEL_OVERWRITE_DELETE: 15 53 * * MEMBER_KICK: 20 54 * * MEMBER_PRUNE: 21 55 * * MEMBER_BAN_ADD: 22 56 * * MEMBER_BAN_REMOVE: 23 57 * * MEMBER_UPDATE: 24 58 * * MEMBER_ROLE_UPDATE: 25 59 * * MEMBER_MOVE: 26 60 * * MEMBER_DISCONNECT: 27 61 * * BOT_ADD: 28, 62 * * ROLE_CREATE: 30 63 * * ROLE_UPDATE: 31 64 * * ROLE_DELETE: 32 65 * * INVITE_CREATE: 40 66 * * INVITE_UPDATE: 41 67 * * INVITE_DELETE: 42 68 * * WEBHOOK_CREATE: 50 69 * * WEBHOOK_UPDATE: 51 70 * * WEBHOOK_DELETE: 52 71 * * EMOJI_CREATE: 60 72 * * EMOJI_UPDATE: 61 73 * * EMOJI_DELETE: 62 74 * * MESSAGE_DELETE: 72 75 * * MESSAGE_BULK_DELETE: 73 76 * * MESSAGE_PIN: 74 77 * * MESSAGE_UNPIN: 75 78 * * INTEGRATION_CREATE: 80 79 * * INTEGRATION_UPDATE: 81 80 * * INTEGRATION_DELETE: 82 81 * @typedef {?number|string} AuditLogAction 82 */ 83 84 /** 85 * All available actions keyed under their names to their numeric values. 86 * @name GuildAuditLogs.Actions 87 * @type {AuditLogAction} 88 */ 89 const Actions = { 90 ALL: null, 91 GUILD_UPDATE: 1, 92 CHANNEL_CREATE: 10, 93 CHANNEL_UPDATE: 11, 94 CHANNEL_DELETE: 12, 95 CHANNEL_OVERWRITE_CREATE: 13, 96 CHANNEL_OVERWRITE_UPDATE: 14, 97 CHANNEL_OVERWRITE_DELETE: 15, 98 MEMBER_KICK: 20, 99 MEMBER_PRUNE: 21, 100 MEMBER_BAN_ADD: 22, 101 MEMBER_BAN_REMOVE: 23, 102 MEMBER_UPDATE: 24, 103 MEMBER_ROLE_UPDATE: 25, 104 MEMBER_MOVE: 26, 105 MEMBER_DISCONNECT: 27, 106 BOT_ADD: 28, 107 ROLE_CREATE: 30, 108 ROLE_UPDATE: 31, 109 ROLE_DELETE: 32, 110 INVITE_CREATE: 40, 111 INVITE_UPDATE: 41, 112 INVITE_DELETE: 42, 113 WEBHOOK_CREATE: 50, 114 WEBHOOK_UPDATE: 51, 115 WEBHOOK_DELETE: 52, 116 EMOJI_CREATE: 60, 117 EMOJI_UPDATE: 61, 118 EMOJI_DELETE: 62, 119 MESSAGE_DELETE: 72, 120 MESSAGE_BULK_DELETE: 73, 121 MESSAGE_PIN: 74, 122 MESSAGE_UNPIN: 75, 123 INTEGRATION_CREATE: 80, 124 INTEGRATION_UPDATE: 81, 125 INTEGRATION_DELETE: 82, 126 }; 127 128 /** 129 * Audit logs entries are held in this class. 130 */ 131 class GuildAuditLogs { 132 constructor(guild, data) { 133 if (data.users) for (const user of data.users) guild.client.users.add(user); 134 /** 135 * Cached webhooks 136 * @type {Collection<Snowflake, Webhook>} 137 * @private 138 */ 139 this.webhooks = new Collection(); 140 if (data.webhooks) { 141 for (const hook of data.webhooks) { 142 this.webhooks.set(hook.id, new Webhook(guild.client, hook)); 143 } 144 } 145 146 /** 147 * Cached integrations 148 * @type {Collection<Snowflake, Integration>} 149 * @private 150 */ 151 this.integrations = new Collection(); 152 if (data.integrations) { 153 for (const integration of data.integrations) { 154 this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); 155 } 156 } 157 158 /** 159 * The entries for this guild's audit logs 160 * @type {Collection<Snowflake, GuildAuditLogsEntry>} 161 */ 162 this.entries = new Collection(); 163 for (const item of data.audit_log_entries) { 164 const entry = new GuildAuditLogsEntry(this, guild, item); 165 this.entries.set(entry.id, entry); 166 } 167 } 168 169 /** 170 * Handles possible promises for entry targets. 171 * @returns {Promise<GuildAuditLogs>} 172 */ 173 static build(...args) { 174 const logs = new GuildAuditLogs(...args); 175 return Promise.all(logs.entries.map(e => e.target)).then(() => logs); 176 } 177 178 /** 179 * The target of an entry. It can be one of: 180 * * A guild 181 * * A user 182 * * A role 183 * * An emoji 184 * * An invite 185 * * A webhook 186 * * An integration 187 * * An object with an id key if target was deleted 188 * * An object where the keys represent either the new value or the old value 189 * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook|Integration} AuditLogEntryTarget 190 */ 191 192 /** 193 * Finds the target type from the entry action. 194 * @param {AuditLogAction} target The action target 195 * @returns {AuditLogTargetType} 196 */ 197 static targetType(target) { 198 if (target < 10) return Targets.GUILD; 199 if (target < 20) return Targets.CHANNEL; 200 if (target < 30) return Targets.USER; 201 if (target < 40) return Targets.ROLE; 202 if (target < 50) return Targets.INVITE; 203 if (target < 60) return Targets.WEBHOOK; 204 if (target < 70) return Targets.EMOJI; 205 if (target < 80) return Targets.MESSAGE; 206 if (target < 90) return Targets.INTEGRATION; 207 return Targets.UNKNOWN; 208 } 209 210 /** 211 * The action type of an entry, e.g. `CREATE`. Here are the available types: 212 * * CREATE 213 * * DELETE 214 * * UPDATE 215 * * ALL 216 * @typedef {string} AuditLogActionType 217 */ 218 219 /** 220 * Finds the action type from the entry action. 221 * @param {AuditLogAction} action The action target 222 * @returns {AuditLogActionType} 223 */ 224 static actionType(action) { 225 if ( 226 [ 227 Actions.CHANNEL_CREATE, 228 Actions.CHANNEL_OVERWRITE_CREATE, 229 Actions.MEMBER_BAN_REMOVE, 230 Actions.BOT_ADD, 231 Actions.ROLE_CREATE, 232 Actions.INVITE_CREATE, 233 Actions.WEBHOOK_CREATE, 234 Actions.EMOJI_CREATE, 235 Actions.MESSAGE_PIN, 236 Actions.INTEGRATION_CREATE, 237 ].includes(action) 238 ) { 239 return 'CREATE'; 240 } 241 242 if ( 243 [ 244 Actions.CHANNEL_DELETE, 245 Actions.CHANNEL_OVERWRITE_DELETE, 246 Actions.MEMBER_KICK, 247 Actions.MEMBER_PRUNE, 248 Actions.MEMBER_BAN_ADD, 249 Actions.MEMBER_DISCONNECT, 250 Actions.ROLE_DELETE, 251 Actions.INVITE_DELETE, 252 Actions.WEBHOOK_DELETE, 253 Actions.EMOJI_DELETE, 254 Actions.MESSAGE_DELETE, 255 Actions.MESSAGE_BULK_DELETE, 256 Actions.MESSAGE_UNPIN, 257 Actions.INTEGRATION_DELETE, 258 ].includes(action) 259 ) { 260 return 'DELETE'; 261 } 262 263 if ( 264 [ 265 Actions.GUILD_UPDATE, 266 Actions.CHANNEL_UPDATE, 267 Actions.CHANNEL_OVERWRITE_UPDATE, 268 Actions.MEMBER_UPDATE, 269 Actions.MEMBER_ROLE_UPDATE, 270 Actions.MEMBER_MOVE, 271 Actions.ROLE_UPDATE, 272 Actions.INVITE_UPDATE, 273 Actions.WEBHOOK_UPDATE, 274 Actions.EMOJI_UPDATE, 275 Actions.INTEGRATION_UPDATE, 276 ].includes(action) 277 ) { 278 return 'UPDATE'; 279 } 280 281 return 'ALL'; 282 } 283 284 toJSON() { 285 return Util.flatten(this); 286 } 287 } 288 289 /** 290 * Audit logs entry. 291 */ 292 class GuildAuditLogsEntry { 293 constructor(logs, guild, data) { 294 const targetType = GuildAuditLogs.targetType(data.action_type); 295 /** 296 * The target type of this entry 297 * @type {AuditLogTargetType} 298 */ 299 this.targetType = targetType; 300 301 /** 302 * The action type of this entry 303 * @type {AuditLogActionType} 304 */ 305 this.actionType = GuildAuditLogs.actionType(data.action_type); 306 307 /** 308 * Specific action type of this entry in its string presentation 309 * @type {AuditLogAction} 310 */ 311 this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); 312 313 /** 314 * The reason of this entry 315 * @type {?string} 316 */ 317 this.reason = data.reason || null; 318 319 /** 320 * The user that executed this entry 321 * @type {User} 322 */ 323 this.executor = guild.client.options.partials.includes(PartialTypes.USER) 324 ? guild.client.users.add({ id: data.user_id }) 325 : guild.client.users.cache.get(data.user_id); 326 327 /** 328 * An entry in the audit log representing a specific change. 329 * @typedef {object} AuditLogChange 330 * @property {string} key The property that was changed, e.g. `nick` for nickname changes 331 * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname 332 * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname 333 */ 334 335 /** 336 * Specific property changes 337 * @type {AuditLogChange[]} 338 */ 339 this.changes = data.changes ? data.changes.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) : null; 340 341 /** 342 * The ID of this entry 343 * @type {Snowflake} 344 */ 345 this.id = data.id; 346 347 /** 348 * Any extra data from the entry 349 * @type {?Object|Role|GuildMember} 350 */ 351 this.extra = null; 352 switch (data.action_type) { 353 case Actions.MEMBER_PRUNE: 354 this.extra = { 355 removed: Number(data.options.members_removed), 356 days: Number(data.options.delete_member_days), 357 }; 358 break; 359 360 case Actions.MEMBER_MOVE: 361 case Actions.MESSAGE_DELETE: 362 case Actions.MESSAGE_BULK_DELETE: 363 this.extra = { 364 channel: guild.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, 365 count: Number(data.options.count), 366 }; 367 break; 368 369 case Actions.MESSAGE_PIN: 370 case Actions.MESSAGE_UNPIN: 371 this.extra = { 372 channel: guild.client.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, 373 messageID: data.options.message_id, 374 }; 375 break; 376 377 case Actions.MEMBER_DISCONNECT: 378 this.extra = { 379 count: Number(data.options.count), 380 }; 381 break; 382 383 case Actions.CHANNEL_OVERWRITE_CREATE: 384 case Actions.CHANNEL_OVERWRITE_UPDATE: 385 case Actions.CHANNEL_OVERWRITE_DELETE: 386 switch (data.options.type) { 387 case 'member': 388 this.extra = guild.members.cache.get(data.options.id) || { id: data.options.id, type: 'member' }; 389 break; 390 391 case 'role': 392 this.extra = guild.roles.cache.get(data.options.id) || { 393 id: data.options.id, 394 name: data.options.role_name, 395 type: 'role', 396 }; 397 break; 398 399 default: 400 break; 401 } 402 break; 403 404 default: 405 break; 406 } 407 408 /** 409 * The target of this entry 410 * @type {?AuditLogEntryTarget} 411 */ 412 this.target = null; 413 if (targetType === Targets.UNKNOWN) { 414 this.target = this.changes.reduce((o, c) => { 415 o[c.key] = c.new || c.old; 416 return o; 417 }, {}); 418 this.target.id = data.target_id; 419 // MEMBER_DISCONNECT and similar types do not provide a target_id. 420 } else if (targetType === Targets.USER && data.target_id) { 421 this.target = guild.client.options.partials.includes(PartialTypes.USER) 422 ? guild.client.users.add({ id: data.target_id }) 423 : guild.client.users.cache.get(data.target_id); 424 } else if (targetType === Targets.GUILD) { 425 this.target = guild.client.guilds.cache.get(data.target_id); 426 } else if (targetType === Targets.WEBHOOK) { 427 this.target = 428 logs.webhooks.get(data.target_id) || 429 new Webhook( 430 guild.client, 431 this.changes.reduce( 432 (o, c) => { 433 o[c.key] = c.new || c.old; 434 return o; 435 }, 436 { 437 id: data.target_id, 438 guild_id: guild.id, 439 }, 440 ), 441 ); 442 } else if (targetType === Targets.INVITE) { 443 this.target = guild.members.fetch(guild.client.user.id).then(me => { 444 if (me.permissions.has('MANAGE_GUILD')) { 445 const change = this.changes.find(c => c.key === 'code'); 446 return guild.fetchInvites().then(invites => { 447 this.target = invites.find(i => i.code === (change.new || change.old)); 448 }); 449 } else { 450 this.target = this.changes.reduce((o, c) => { 451 o[c.key] = c.new || c.old; 452 return o; 453 }, {}); 454 return this.target; 455 } 456 }); 457 } else if (targetType === Targets.MESSAGE) { 458 // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. 459 this.target = 460 data.action_type === Actions.MESSAGE_BULK_DELETE 461 ? guild.channels.cache.get(data.target_id) || { id: data.target_id } 462 : guild.client.users.cache.get(data.target_id); 463 } else if (targetType === Targets.INTEGRATION) { 464 this.target = 465 logs.integrations.get(data.target_id) || 466 new Integration( 467 guild.client, 468 this.changes.reduce( 469 (o, c) => { 470 o[c.key] = c.new || c.old; 471 return o; 472 }, 473 { id: data.target_id }, 474 ), 475 guild, 476 ); 477 } else if (data.target_id) { 478 this.target = guild[`${targetType.toLowerCase()}s`].cache.get(data.target_id) || { id: data.target_id }; 479 } 480 } 481 482 /** 483 * The timestamp this entry was created at 484 * @type {number} 485 * @readonly 486 */ 487 get createdTimestamp() { 488 return Snowflake.deconstruct(this.id).timestamp; 489 } 490 491 /** 492 * The time this entry was created at 493 * @type {Date} 494 * @readonly 495 */ 496 get createdAt() { 497 return new Date(this.createdTimestamp); 498 } 499 500 toJSON() { 501 return Util.flatten(this, { createdTimestamp: true }); 502 } 503 } 504 505 GuildAuditLogs.Actions = Actions; 506 GuildAuditLogs.Targets = Targets; 507 GuildAuditLogs.Entry = GuildAuditLogsEntry; 508 509 module.exports = GuildAuditLogs;