buddy

node MVC discord bot
Log | Files | Refs | README

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;