'Lodash.cloneDeep: unable to edit property of cloned object [discord.js execute command]

I'm currently trying to make an ~execute command for my bot. This command will allow me to execute commands as other users, thereby allowing to provide better debugging.

An example of this would be something like:

Example of the ~execute command in use, on an already-working instance.

In essence, what I want/need to do in this command is:

  1. grab the message object
  2. clone the message object
  3. edit the cloned message object such that its author and member are different, but everything else remains the same.
  4. pass this new object through client.emit("messageCreate", <NewMessageObject>).

I've tried the following...

JSON.parse(JSON.stringify(...))

This one was actually the most promising, however, it did not copy over all the "special" stuff -- during my research, I learned that this method only works well for primitive data types.

const cloned = JSON.parse(JSON.stringify(message));
// the normal functions/methods such as message.reply and message.guild.members.fetch were undefined.

By "most promising", I mean that this method actually copied the guild object and everything else.

Note that JSON method will loose any Javascript types that have no equivalent in JSON. For example: JSON.parse(JSON.stringify({a:null,b:NaN,c:Infinity,d:undefined,e:function(){},f:Number,g:false})) will generate {a: null, b: null, c: null, g: false} – @oriadam

Source: a comment on this question (What is the most efficient way to deep clone an object in JavaScript?)

Lodash's cloneDeep()

I found What is the most efficient way to deep clone an object in JavaScript?, and saw this. I tried it, however, it doesn't seem to work. By "doesn't seem to work", I mean that when I attempt to reassign certain properties, it doesn't do anything. Here's my current code, which should hopefully make it easier to understand what I mean:

// this is the code inside my command. I've removed the fluff to make it easier to read. It is inside an async function.
const { cloneDeep } = require("lodash");
const user = await client.config.fetchUser(args[0]); // works fine -- identifies user perfectly. eg User { id: '' ... }
if (!user) return message.reply({ content: `Invalid user "${args[0]}"`, allowedMentions: { parse: [] } });
console.log(cloneDeep(message)) // again, this bit is also fine - it logs a message object.
const cloned = cloneDeep(message);
cloned.author = user;
console.log(cloned.author); // everything works fine - the user is changed fine.
cloned.content = message.guild.prefix + args.slice(1).join(" ");
cloned.emit = true;
client.emit("messageCreate", cloned);

Note: the only 2 properties that were changed are: author and content. Everything else in the message object should remain the same, such as guild, channel, etc.

I'm getting this error:

TypeError: Cannot read properties of undefined (reading 'guilds') [index.js:478:15]

enter image description here

The error is pointing to this line in the messageCreate event:

    if (!message.guild || (message.author.bot && (!cst.includes("wl"))) || (message.system) || (message.webhookId)) return;

From what I gathered, in this context, the error basically meant that message.guild (and, in turn) message was undefined. (as we were parsing the cloned object into the messageCreate event. This isn't an issue with anything else, as all other commands work fine. Even ones done in dms.)

Next, I added console.log(cloned.guild) to the code, expecting a Guild object. Instead, I got the same error message:

TypeError: Cannot read properties of undefined (reading 'guilds').

Simply adding cloned.guild = message.guild; has not seemed to fix the issue. It has resulted in the same error. I have checked the value of message.guild, and it is indeed a Guild object.

Here's the final code which I'm currently working with:

./cmds/execute.js

const { Message } = require("discord.js");
const { cloneDeep } = require("lodash");

module.exports = {
    name: "execute",
    aliases: ["execute", "exec"],
    async run(client, message, args) {
        if (args.length < 2) return message.reply("You must specify a user and a command to execute as the user in order for this command to work!");
        const user = await client.config.fetchUser(args[0]);
        if (!user) return message.reply({ content: `Invalid user "${args[0]}"`, allowedMentions: { parse: [] } });
        const cloned = cloneDeep(message);
        cloned.author = user;
        cloned.content = message.guild.prefix + args.slice(1).join(" ");
        cloned.emit = true;
        // works FINE till here. For some reason, cloned.guild just doesn't work...
        cloned.guild = message.guild;
        client.emit("messageCreate", cloned);
    },
};

This is what a properly formed message object should look like:

<ref *1> Message {
  channelId: '911802238442287124',
  guildId: '911784758600679455',
  deleted: false,
  id: '920762096373891134',
  createdTimestamp: 1639597190708,
  type: 'DEFAULT',
  system: false,
  content: '~eval message',
  author: User {
    id: '501710994293129216',
    bot: false,
    system: false,
    flags: [UserFlags],
    username: 'Paradox',
    discriminator: '1234',
    avatar: 'fd6c2479694970c0a357155bd43860d4',
    banner: undefined,
    accentColor: undefined,
    debug: false, // this is a custom property that I've attached myself - isnt here by default. same with color and colors.
    color: 'RANDOM',
    colors: [Array]
  },
  pinned: false,
  tts: false,
  nonce: '920762095794913280',
  embeds: [],
  components: [],
  attachments: Collection(0) [Map] {},
  stickers: Collection(0) [Map] {},
  editedTimestamp: null,
  reactions: ReactionManager { message: [Circular *1] },
  mentions: MessageMentions {
    everyone: false,
    users: Collection(0) [Map] {},
    roles: Collection(0) [Map] {},
    _members: null,
    _channels: null,
    crosspostedChannels: Collection(0) [Map] {},
    repliedUser: null
  },
  webhookId: null,
  groupActivityApplication: null,
  applicationId: null,
  activity: null,
  flags: MessageFlags { bitfield: 0 },
  reference: null,
  interaction: null
}

And this is what the deepCloned one looks like:

<ref *1> Message {
  channelId: '911802238442287124',
  guildId: '911784758600679455',
  deleted: false,
  id: '920762763272392715',
  createdTimestamp: 1639597349709,
  type: 'DEFAULT',
  system: false,
  content: '~bal',
  author: User {
    id: '504619833007013899',
    bot: false,
    system: false,
    flags: UserFlags { bitfield: 0 },
    username: 'ephemeral',
    discriminator: '2341',
    avatar: '2f274547855bf700b44408c593d37cad',
    banner: undefined,
    accentColor: undefined
  },
  pinned: false,
  tts: false,
  nonce: '920762762311761920',
  embeds: [],
  components: [],
  attachments: Collection(0) [Map] {},
  stickers: Collection(0) [Map] {},
  editedTimestamp: null,
  reactions: ReactionManager { message: [Circular *1] },
  mentions: MessageMentions {
    everyone: false,
    users: Collection(0) [Map] {},
    roles: Collection(0) [Map] {},
    _members: Collection(0) [Map] {},
    _channels: null,
    crosspostedChannels: Collection(0) [Map] {},
    repliedUser: null
  },
  webhookId: null,
  groupActivityApplication: null,
  applicationId: null,
  activity: null,
  flags: MessageFlags { bitfield: 0 },
  reference: null,
  interaction: null,
  emit: true
}

PACKAGE.JSON DEPENDENCY VERSIONS:

  "dependencies": {
    "@keyv/mysql": "^1.1.4",
    "@keyv/sqlite": "^2.0.2",
    "babel-plugin-syntax-class-properties": "^6.13.0",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "bootstrap": "^4.5.2",
    "delay": "^4.4.1",
    "discord.js": "^13.0.0",
    "discord.js-reaction-menu": "^1.0.2",
    "dotenv": "^8.2.0",
    "enmap": "github:eslachance/enmap#v3",
    "enmap-mongo": "^2.0.2",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "helmet": "^4.1.1",
    "keyv": "^4.0.0",
    "lodash": "^4.17.21",
    "lodash.clonedeep": "^4.5.0",
    "mathjs": "^7.2.0",
    "moment": "^2.27.0",
    "mongochrome": "0.0.3",
    "mongoose": "^5.10.7",
    "morgan": "^1.10.0",
    "ms": "^2.1.3",
    "mysql": "^2.18.1",
    "node-fetch": "^2.6.0",
    "node-os-utils": "^1.2.2",
    "passport": "^0.4.1",
    "passport-discord": "^0.1.4",
    "pug": "^3.0.0",
    "quickmongo": "^2.0.1",
    "serve-favicon": "^2.5.0",
    "sqlite3": "^5.0.2",
    "statcord.js": "^3.1.0",
    "uptime-robot": "^1.3.0"
  }

node: v16.10.0

I've tried my best to construct a good question. Apologies if this is unclear. If any further information is required, please do let me know.

Edit: I have already tried Object.assign(target, ...sources) and that doesn't work either - as Object.assign only performs a shallow clone, and not a deep one.



Solution 1:[1]

The issue

I says it in the error: the cloned object is undefined in the event handler listening for the messageCreate event. You are trying to read a property from undefined, just like the error told you.

The way you pass this object using client.emit("messageCreate", cloned) is wrong, the cloned object seems to not be passed along to the listener, this has nothing to do with the guild property.

You also don't need to set it using cloned.guild = message.guild, it should already be there.

Fix

Pass the new message object in another way to your command handler (calling the function directly from this command file should do the trick but you didn't send any code about that so i can't help you), creating a messageCreate event is not a good idea.

Note: Maybe don't use lodash, it may create some internal references in the sub properties, or behave in an unexpected way, while the JSON trick will do just what you need and save you a headache, plus you don't need "the fastest way to deepclone an object" for a debugging command.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Shyne