Source: webhook.js

const MessageBuilder = require("./builder");

const snekfetch = require("snekfetch");

const ENDPOINT = "https://discordapp.com/api/v7/webhooks";

class Webhook {
    /**
    @constructor
    @param {string} url The webhooks url.
    */
    constructor(url, nowait = false) {
        this.url = url;
        this.id = "";
        this.token = "";
        this.meta = {};
        this.ready = false;
        this.nowait = nowait;
        snekfetch.get(this.url)
            .then((res) => {
                let parsed = JSON.parse(res.text);
                Object.assign(this.meta, parsed);
                this.id = parsed.id;
                this.token = parsed.token;
                this.ready = true;
            })
            .catch((err) => {
                throw err;
            });
    }

    /**
     * Verify the name of a webhook.
     * @param {string} name The name of the webhook
     */
    verifyName(name) {
        if (name.length > 32) {
            throw new Error(`The name specified was ${name.length} characters. Discord webhook names must be under 32 characters`);
        }
    }


    /**
     * Check a fields value length, because discord is rejecting fields containing more than 1024 chararacters in a value.
     * @param {string} value 
     */
    checkFieldValueLength(value) {
        if (value.length > 1024)
            throw new Error(`The value given was ${value.length} characters, fields of Discord embeds must be under 1024 characters`);
    }


    /**
     * Checks every field in the only possible attachment.
     * @param {fields} fields 
     */
    checkFields(payload) {
        if (!payload) {
            return new Promise((resolve) => resolve());
        }

        if (Object.keys(payload).indexOf("fields") == -1) {
            return new Promise((resolve) => resolve());
        }

        return new Promise((resolve) => {
            const checks = [];

            for (var i = 0; i < payload.fields.length; i++) {
                checks.push(this.checkFieldValueLength(payload.fields[i].value));
            }

            Promise.all(checks).then(() => {
                resolve();
            });
        });

    }

    /**
    @param {object} payloadRaw The payload to send.
    @param {function} resolveFunction Used internally for ratelimiting handling
    */
    sendPayload(payloadRaw, resolveFunction) {
        let payload = this.sanitiseEmbeds(payloadRaw);
        this.verifyName(payload.username);
        this.checkFields(payload.attachments[0]).then(() => {
            if (this.ready) {
                return new Promise((resolve, reject) => {
                    snekfetch.post(this.endpoint)
                        .send(payload)
                        .then((response) => {
                            if (resolveFunction) {
                                resolveFunction(response);
                            }

                            resolve();
                        })
                        .catch((err) => {
                            let e = JSON.parse(err.text);

                            if (e.code === 50006) { // We've sent an empty message
                                reject({ error: "No content specified" });
                            }

                            if (e.retry_after) { // We are being ratelimited
                                setTimeout(() => {
                                    this.sendPayload(payload, resolve);
                                }, e.retry_after);
                            } else {
                                reject(err);
                            }
                        });
                });
            } else {
                setTimeout(() => {
                    this.sendPayload(payload, resolveFunction);
                }, 10);
            }
        });

    }

    /**
    @returns {string} The endpoint of the webhook.
    */
    get endpoint() {
        let qs = "";
        if (this.nowait) {
            qs = "?wait=false";
        }
        return `${ENDPOINT}/${this.id}/${this.token}/slack${qs}`;
    }

    /**
    @param {string} name The username of the error webhook.
    @param {string} message The message of the error webhook.
    */
    err(name, message) {
        let payload = {
            "username": name,
            "attachments": [{
                "color": "#ff0000",
                "fields": [{
                    "title": "Error",
                    "value": message
                }],
                "ts": new Date() / 1000
            }]
        };

        return this.sendPayload(payload);
    }

    /**
    @param {string} name The username of the info webhook.
    @param {string} message The message of the info webhook.
    */
    info(name, message) {
        let payload = {
            "username": name,
            "attachments": [{
                "color": "#00fffa",
                "fields": [{
                    "title": "Information",
                    "value": message
                }],
                "ts": new Date() / 1000
            }]
        };

        return this.sendPayload(payload);
    }
    /**
    @param {string} name The username of the success webhook.
    @param {string} message The message of the success webhook.
    */
    success(name, message) {
        let payload = {
            "username": name,
            "attachments": [{
                "color": "#04ff00",
                "fields": [{
                    "title": "Success",
                    "value": message
                }],
                "ts": new Date() / 1000
            }]
        };

        return this.sendPayload(payload);
    }
    /**
    @param {string} name The username of the warning webhook.
    @param {string} message The message of the warning webhook.
    */
    warn(name, message) {
        let payload = {
            "username": name,
            "attachments": [{
                "color": "#ffe900",
                "fields": [{
                    "title": "Warning",
                    "value": message
                }],
                "ts": new Date() / 1000
            }]
        };

        return this.sendPayload(payload);
    }

    /**
    @param {string} name The username of the custom webhook.
    @param {string} message The message of the custom webhook.
    @param {string} title The title of the custom webhook.
    @param {string} color The color of the custom webhook. (optional)
    @param {string} imageUrl The url of the image to attach (optional)
    */
    custom(name, message, title, color, imageUrl) {
        let payload = {
            "username": name,
            "attachments": [{
                "fields": [{
                    "title": title,
                    "value": message
                }],
                "ts": new Date() / 1000
            }]
        };
        if (color) {
            payload.attachments[0].color = color;
        }
        if (imageUrl) {
            payload.attachments[0].image_url = imageUrl;
        }
        return this.sendPayload(payload);
    }

    /**
    @param {string} name The username of the webhook
    @param {string} message The message for the webhook to send
    @param {string} title The title of the message
    @param {string} imageUrl The URL of the image to attach
    */
    image(name, message, title, imageUrl) {
        let payload = {
            "username": name,
            "attachments": [{
                "fields": [{
                    "title": title,
                    "value": message
                }],
                "ts": new Date() / 1000,
                "image_url": imageUrl
            }]
        };

        return this.sendPayload(payload);
    }

    /**
     * Remove embeds from a message if only text is specified
     *
     * @param {Object} data The Discord webhook data
     */
    sanitiseEmbeds(data) {
        // Need to copy a derefenced object so we don't touch the messagebuilder
        let newPayload = Object.assign({}, data);

        if (data.attachments.length > 0) {
            if (
                Object.keys(data.attachments[0]).length > 1 ||
                data.attachments[0].fields.length > 0
            ) {
                // There is an embed
                return newPayload;
            } else {
                // No embeds
                newPayload.attachments = [];
                return newPayload;
            }
        } else {
            return newPayload;
        }
    }

    /**
     * This method is used to send a MessageBuilder to the webhook.
     *
     * @param {MessageBuilder} messageBuilder The message.
     */
    send(messageBuilder) {
        return new Promise((resolve, reject) => {
            // Discord will return an angry error if we don't specify a username
            if (!messageBuilder.data.username) {
                reject({ error: "No username provided" });
            }


            if (
                messageBuilder.data.text // Check for any text
            ) {
                return this.sendPayload(messageBuilder.data, resolve);
            } else if (
                Object.keys(messageBuilder.data.attachments[0]).length > 1 || // Check if there are any other properties in the embed (fields will always be there)
                messageBuilder.data.attachments[0].fields.length > 0 // Check if there are any fields in the emebd
            ) {
                return this.sendPayload(messageBuilder.data, resolve);
            } else {
                reject({ error: "No content specified" });
            }
        });
    }
}

module.exports = {
    Webhook,
    MessageBuilder
};