Search code examples
javascriptnode.jsxmldiscord.jsbots

Using node/discord.js, how can I get my discord bot to parse an XML file and produce contents from within one of its tags as a response?


I'm trying to build a /lookup command that will read from a bunch of pre-prepared XML files and produce the contents as an embed response.

Ultimately, the syntax will be /lookup -> choose category -> choose name; e.g. /lookup 'spell' -> 'acid splash' -> bot delivers an embed with details parsed from the 'spells' XML file.

I'm having trouble getting the proof of concept together.

The XMLs all follow this same general structure (this is the contents of /xml/a.xml from my example below):

<spell>
        <name>Acid Splash</name>
        <level>0</level>
        <school>C</school>
        <ritual>NO</ritual>
        <time>1 action</time>
        <range>60 feet</range>
        <components>V, S</components>
        <duration>Instantaneous</duration>
        <classes>Fighter (Eldritch Knight), Rogue (Arcane Trickster), Sorcerer, Wizard, Artificer</classes>
        <text>You hurl a bubble of acid. Choose one creature you can see within range, or choose two creatures you can see within range that are within 5 feet of each other. A target must succeed on a Dexterity saving throw or take 1d6 acid damage.</text>
        <text>  This spells damage increases by 1d6 when you reach 5th Level (2d6), 11th level (3d6) and 17th level (4d6).</text>
        <text/>
        <text>Source: Player's Handbook p. 211</text>
        <roll>1d6</roll>
</spell>

This example has 2 different attempts (one is commented out), and both are arguably closer to my goal, but still not quite working as expected:

const { SlashCommandBuilder } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
  .setName('test5')
  .setDescription('returns the contents of an xml file'),

  async execute(interaction){
    
    fs = require('fs');
    var parser = require('xml2json-light');

    fs.readFile( 'xml/a.xml', function(err, data) {

    // Attempt A
    // This version doesn't error, but it also doesn't produce a response...
    var xml = '<person><name>John Doe</name></person>';
    console.log("to json ->", xml);
    interaction.reply("to json ->", xml);
    
    // // Attempt B
    // // This version produces the attached error...
    // var json = parser.xml2json(data);
    // console.log("to json ->", data);
    // interaction.reply("to json ->", data);
    });
  }
}

The console log from Attempt A:

// NOTE: I CAN'T GET ANYTHING AFTER THE -> TO PRODUCE AS A RESPONSE FROM THE BOT BECAUSE IT'S REGARDED AS 'UNDEFINED'...

to json -> <person><name>John Doe</name></person>

The response my bot gives to Attempt A, however, as--according to err--it regards 'data' as 'undefined':

The response my bot gives to Attempt A

The error produced by Attempt B:

// NOTE: THERE MAY BE COMMENTED OUT LINES WHERE I ATTEMPTED TO TROUBLESHOOT THIS IN XML2JSON.JS...

*************************Smashmouth-2\node_modules\xml2json-light\xml2json.js:67
    xmlStr = xmlStr.replace( /<!--[\s\S]*?-->/g, '' ); //remove commented lines
                    ^

TypeError: xmlStr.replace is not a function
    at cleanXML (C:\Users\snake\OneDrive\Documents\discord\Smashmouth-2\node_modules\xml2json-light\xml2json.js:67:21)
    at Object.xml2json (C:\Users\snake\OneDrive\Documents\discord\Smashmouth-2\node_modules\xml2json-light\xml2json.js:11:14)
    at C:\Users\snake\OneDrive\Documents\discord\Smashmouth-2\commands\utility\test5.js:23:23
    at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read_file_context:68:3)

The contents of /node_modules/xml2json-light/xml2json.js, which may have some commented out lines where I attempted to troubleshoot to no avail:

// NOTE: THERE MAY BE COMMENTED OUT LINES WHERE I ATTEMPTED TO TROUBLESHOOT THIS IN HERE...

'use strict';

module.exports = {
    xml2json: xml2json
};

//***********************************************************************
// Main function. Clears the given xml and then starts the recursion
//***********************************************************************
function xml2json(xmlStr){ 
    xmlStr = cleanXML(xmlStr);
    return xml2jsonRecurse(xmlStr,0); 
}

//***********************************************************************
// Recursive function that creates a JSON object with a given XML string.
//***********************************************************************
function xml2jsonRecurse(xmlStr) {
    var obj = {},
        tagName, indexClosingTag, inner_substring, tempVal, openingTag;

    // if(!xmlStr) return;
    while (xmlStr.match(/<[^\/][^>]*>/)) {
        openingTag = xmlStr.match(/<[^\/][^>]*>/)[0];
        tagName = openingTag.substring(1, openingTag.length - 1);
        indexClosingTag = xmlStr.indexOf(openingTag.replace('<', '</'));

        // account for case where additional information in the openning tag
        if (indexClosingTag == -1) {

            tagName = openingTag.match(/[^<][\w+$]*/)[0];
            indexClosingTag = xmlStr.indexOf('</' + tagName);
            if (indexClosingTag == -1) {
                indexClosingTag = xmlStr.indexOf('<\\/' + tagName);
            }
        }
        inner_substring = xmlStr.substring(openingTag.length, indexClosingTag);
        if (inner_substring.match(/<[^\/][^>]*>/)) {
            tempVal = xml2json(inner_substring);
        }
        else {
            tempVal = inner_substring;
        }
        // account for array or obj //
        if (obj[tagName] === undefined) {
            obj[tagName] = tempVal;
        }
        else if (Array.isArray(obj[tagName])) {
            obj[tagName].push(tempVal);
        }
        else {
            obj[tagName] = [obj[tagName], tempVal];
        }

        xmlStr = xmlStr.substring(openingTag.length * 2 + 1 + inner_substring.length);
    }

    return obj;
}

//*****************************************************************
// Removes some characters that would break the recursive function.
//*****************************************************************
function cleanXML(xmlStr) {
    
    // if(!xmlStr) return;
    xmlStr = xmlStr.replace( /<!--[\s\S]*?-->/g, '' ); //remove commented lines
    xmlStr = xmlStr.replace(/\n|\t|\r/g, ''); //replace special characters
    xmlStr = xmlStr.replace(/ {1,}<|\t{1,}</g, '<'); //replace leading spaces and tabs
    xmlStr = xmlStr.replace(/> {1,}|>\t{1,}/g, '>'); //replace trailing spaces and tabs
    xmlStr = xmlStr.replace(/<\?[^>]*\?>/g, ''); //delete docType tags

    xmlStr = replaceSelfClosingTags(xmlStr); //replace self closing tags
    xmlStr = replaceAloneValues(xmlStr); //replace the alone tags values
    xmlStr = replaceAttributes(xmlStr); //replace attributes

    return xmlStr;
}

//************************************************************************************************************
// Replaces all the self closing tags with attributes with another tag containing its attribute as a property.
// The function works if the tag contains multiple attributes. 
//
// Example : '<tagName attrName="attrValue" />' becomes 
//           '<tagName><attrName>attrValue</attrName></tagName>'
//************************************************************************************************************
function replaceSelfClosingTags(xmlStr) {

    // if(!xmlStr) return;
    var selfClosingTags = xmlStr.match(/<[^/][^>]*\/>/g);

    if (selfClosingTags) {
        for (var i = 0; i < selfClosingTags.length; i++) {

            var oldTag = selfClosingTags[i];
            var tempTag = oldTag.substring(0, oldTag.length - 2);
            tempTag += ">";

            var tagName = oldTag.match(/[^<][\w+$]*/)[0];
            var closingTag = "</" + tagName + ">";
            var newTag = "<" + tagName + ">";

            var attrs = tempTag.match(/(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?/g);

            if (attrs) {
                for(var j = 0; j < attrs.length; j++) {
                    var attr = attrs[j];
                    var attrName = attr.substring(0, attr.indexOf('='));
                    var attrValue = attr.substring(attr.indexOf('"') + 1, attr.lastIndexOf('"'));
                    
                    newTag += "<" + attrName + ">" + attrValue + "</" + attrName + ">";
                }
            }

            newTag += closingTag;

            xmlStr = xmlStr.replace(oldTag, newTag);
        }
    }

    return xmlStr;
}

//*************************************************************************************************
// Replaces all the tags with attributes and a value with a new tag.
// 
// Example : '<tagName attrName="attrValue">tagValue</tagName>' becomes 
//           '<tagName><attrName>attrValue</attrName><_@attribute>tagValue</_@attribute></tagName>'
//*************************************************************************************************
function replaceAloneValues(xmlStr) {

    // if(!xmlStr) return;
    var tagsWithAttributesAndValue = xmlStr.match(/<[^\/][^>][^<]+\s+.[^<]+[=][^<]+>{1}([^<]+)/g);
    
    if (tagsWithAttributesAndValue) {
        for(var i = 0; i < tagsWithAttributesAndValue.length; i++) {

            var oldTag = tagsWithAttributesAndValue[i];
            var oldTagName = oldTag.substring(0, oldTag.indexOf(">") + 1);
            var oldTagValue = oldTag.substring(oldTag.indexOf(">") + 1);
            
            var newTag = oldTagName + "<_@ttribute>" + oldTagValue + "</_@ttribute>";
            
            xmlStr = xmlStr.replace(oldTag, newTag);
        }    
    }
    
    return xmlStr;
}

//*****************************************************************************************************************
// Replaces all the tags with attributes with another tag containing its attribute as a property.
// The function works if the tag contains multiple attributes.
//
// Example : '<tagName attrName="attrValue"></tagName>' becomes '<tagName><attrName>attrValue</attrName></tagName>'
//*****************************************************************************************************************
function replaceAttributes(xmlStr) {

    // if(!xmlStr) return;
    var tagsWithAttributes = xmlStr.match(/<[^\/][^>][^<]+\s+.[^<]+[=][^<]+>/g);

    if (tagsWithAttributes) {
        for (var i = 0; i < tagsWithAttributes.length; i++) {
           
            var oldTag = tagsWithAttributes[i];
            var tagName = oldTag.match(/[^<][\w+$]*/)[0];
            var newTag = "<" + tagName + ">";
            var attrs = oldTag.match(/(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?/g);

            if (attrs) {
                for(var j = 0; j < attrs.length; j++) {
                    
                    var attr = attrs[j];
                    var attrName = attr.substring(0, attr.indexOf('='));
                    var attrValue = attr.substring(attr.indexOf('"') + 1, attr.lastIndexOf('"'));
                    
                    newTag += "<" + attrName + ">" + attrValue + "</" + attrName + ">";
                }
            }

            xmlStr = xmlStr.replace(oldTag, newTag);
        }
    }

    return xmlStr;
}

Solution

  • About Attempt A:
    interaction.reply does not support passing a string as the second argument. Therefore, you must concatenate the strings in the first argument.

    About Attempt B:
    interaction.reply does not support passing a string as its second argument:
    This means that no string is passed to parser.xml2json. That is, data is not a string. You must use toString to make it a string. (Perhaps data is a Buffer).

    const { SlashCommandBuilder } = require('discord.js');
    const fs = require('fs');
    const parser = require('xml2json-light');
    
    module.exports = {
      data: new SlashCommandBuilder()
      .setName('test5')
      .setDescription('returns the contents of an xml file'),
    
      async execute(interaction){
        
        fs.readFile( 'xml/a.xml', function(err, data) {
    
        // Attempt A
        const xml = '<person><name>John Doe</name></person>';
        console.log("to json ->", xml);
        interaction.reply("to json ->" + xml);
        
        // // Attempt B
        // var json = parser.xml2json(data);
        // console.log("to json ->", data.toString());
        // interaction.reply("to json ->" + data.toString());
        });
      }
    }