Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application Commands Part 6: Migration Poll #120

Closed
7 tasks
diewellenlaenge opened this issue Oct 2, 2021 · 0 comments
Closed
7 tasks

Application Commands Part 6: Migration Poll #120

diewellenlaenge opened this issue Oct 2, 2021 · 0 comments
Labels
enhancement New feature or request spacktoberfest Label für Hacktoberfest, damit uns keiner auf die Nüsse geht

Comments

@diewellenlaenge
Copy link
Collaborator

diewellenlaenge commented Oct 2, 2021

Bedingungen

Beschreibung

Das Poll-Modul ist gerade schon groß und wird in Zukunft noch größer. Es soll für eine Umfrage mehrere Modi geben. So soll zum Beispiel die bisherige Funktionalität aus .vote und .extend hier mit einfließen.

Modi

  • multi: bekannter Standardmodus
  • straw: Strawmodus mit mehreren Möglichkeiten
  • list: wie multi, nur dass auch nur eine Antwortmöglichkeit erlaubt ist und soll automatisch immer erweiterbar sein
  • vote: wie heutiges .vote (also nur ja/nein)

Todos

  • Modi einbauen
  • .vote entfernen
  • .extend einbauen
  • Anonyme Umfragen erlauben
  • Umfragen mit Endzeitpunkt erlauben
  • Anonymität soll auch ohne Endzeitpunkt möglich sein
  • Restricted Mode: man soll seine Stimme nicht zurückziehen können wenn gewünscht

Beispiele

Aus dem Fork (bisher nicht committed):

Code (poll.ts)
"use strict";

import { VerifiedCommandInteraction, Result } from "../types";

// ========================= //
// = Copyright (c) NullDev = //
// ========================= //

// Dependencies
let moment = require("moment");
let parseOptions = require("minimist");
let cron = require("node-cron");
const AdditionalMessageData = require("../storage/model/AdditionalMessageData");
const logger = require("../utils/logger");

// Utils
let config = require("../utils/configHandler").getConfig();

const NUMBERS = [
    ":one:",
    ":two:",
    ":three:",
    ":four:",
    ":five:",
    ":six:",
    ":seven:",
    ":eight:",
    ":nine:",
    ":keycap_ten:"
];

const EMOJI = [
    "1️⃣",
    "2️⃣",
    "3️⃣",
    "4️⃣",
    "5️⃣",
    "6️⃣",
    "7️⃣",
    "8️⃣",
    "9️⃣",
    "🔟"
];

/**
 * @typedef {Object} DelayedPoll
 * @property {String} pollId
 * @property {Date} createdAt
 * @property {Date} finishesAt
 * @property {string[][]} reactions
 * @property {string[]} reactionMap
 */

/**
 * @type {DelayedPoll[]}
 */
exports.delayedPolls = [];

/**
 * Creates a new poll (multiple answers) or strawpoll (single selection)
 *
 * @param {import("discord.js").Client} client
 * @param {import("discord.js").Message} message
 * @param {Array} args
 * @param {Function} callback
 * @returns {Function} callback
 */
//exports.run = (client, message, args, callback) => {
async function handler(interaction: VerifiedCommandInteraction): Promise<Result> {
    let options = parseOptions(args, {
        "boolean": [
            "channel",
            "extendable",
            "straw"
        ],
        string: [
            "delayed"
        ],
        alias: {
            channel: "c",
            extendable: "e",
            straw: "s",
            delayed: "d"
        }
    });

    let parsedArgs = options._;
    let delayTime = Number(options.delayed);

    if (!parsedArgs.length) return callback("Bruder da ist keine Umfrage :c");

    let pollArray = parsedArgs.join(" ").split(";").map(e => e.trim()).filter(e => e.replace(/\s/g, "") !== "");
    let pollOptions = pollArray.slice(1);

    if (!pollOptions.length) return callback("Bruder da sind keine Antwortmöglichkeiten :c");
    else if (pollOptions.length < 2) return callback("Bruder du musst schon mehr als eine Antwortmöglichkeit geben 🙄");
    else if (pollOptions.length > 10) return callback("Bitte gib nicht mehr als 10 Antwortmöglichkeiten an!");


    let optionstext = "";
    pollOptions.forEach((e, i) => (optionstext += `${NUMBERS[i]} - ${e}\n`));

    let finishTime = new Date(new Date().valueOf() + (delayTime * 60 * 1000));
    if(options.delayed) {
        if(isNaN(delayTime) || delayTime <= 0) {
            return callback("Bruder keine ungültigen Zeiten angeben 🙄");
        }
        else if(delayTime > 60 * 1000 * 24 * 7) {
            return callback("Bruder du kannst maximal 7 Tage auf ein Ergebnis warten 🙄");
        }
        // Haha oida ist das cancer
        optionstext += `\nAbstimmen möglich bis ${new Date(finishTime.valueOf() + 60000).toLocaleTimeString("de").split(":").splice(0, 2).join(":")}`;
    }

    let embed = {
        embed: {
            title: pollArray[0],
            description: optionstext,
            timestamp: moment.utc().format(),
            author: {
                name: `${options.straw ? "Strawpoll" : "Umfrage"} von ${message.author.username}`,
                icon_url: message.author.displayAvatarURL()
            }
        }
    };

    let footer = [];
    let extendable = options.extendable && pollOptions.length < 10;

    if (extendable) {
        if(options.delayed) {
            return callback("Bruder du kannst -e nicht mit -d kombinieren. 🙄");
        }

        footer.push("Erweiterbar mit .extend als Reply");
        embed.embed.color = "GREEN";
    }

    if(options.delayed) {
        footer.push("⏳");
        embed.embed.color = "#a10083";
    }

    if (!options.straw) footer.push("Mehrfachauswahl");

    if (footer.length) {
        embed.embed.footer = {
            text: footer.join(" • ")
        };
    }

    let voteChannel = client.guilds.cache.get(config.ids.guild_id).channels.cache.get(config.ids.votes_channel_id);
    let channel = options.channel ? voteChannel : message.channel;
    if(options.delayed && channel !== voteChannel) {
        return callback("Du kannst keine verzögerte Abstimmung außerhalb des Umfragenchannels machen!");
    }

    /** @type {import("discord.js").TextChannel} */
    (channel).send(/** @type {Object} embed */(embed))
        .then(async msg => {
            message.delete();
            for (let i in pollOptions) await msg.react(EMOJI[i]);

            if(options.delayed) {
                const reactionMap = [];
                /** @type {string[][]} */
                const reactions = [];
                pollOptions.forEach((option, index) => {
                    reactionMap[index] = option;
                    reactions[index] = [];
                });

                let delayedPollData = {
                    pollId: msg.id,
                    createdAt: new Date().valueOf(),
                    finishesAt: finishTime.valueOf(),
                    reactions,
                    reactionMap
                };

                let additionalData = await AdditionalMessageData.fromMessage(msg);
                let newCustomData = additionalData.customData;
                newCustomData.delayedPollData = delayedPollData;
                additionalData.customData = newCustomData;
                await additionalData.save();

                exports.delayedPolls.push(delayedPollData);
            }
        });

    return callback();
};

exports.importPolls = async() => {
    let additionalDatas = await AdditionalMessageData.findAll();
    let count = 0;
    additionalDatas.forEach(additionalData => {
        if(!additionalData.customData.delayedPollData) {
            return;
        }

        exports.delayedPolls.push(additionalData.customData.delayedPollData);
        count++;
    });
    logger.info(`Loaded ${count} polls from database`);
};

/**
 * Initialized crons for delayed polls
 * @param {import("discord.js").Client} client
 */
exports.startCron = (client) => {
    cron.schedule("* * * * *", async() => {
        const currentDate = new Date();
        const pollsToFinish = exports.delayedPolls.filter(delayedPoll => currentDate >= delayedPoll.finishesAt);
        /** @type {import("discord.js").TextChannel} */
        const channel = client.guilds.cache.get(config.ids.guild_id).channels.cache.get(config.ids.votes_channel_id);

        for(let i = 0; i < pollsToFinish.length; i++) {
            const delayedPoll = pollsToFinish[i];
            const message = await channel.messages.fetch(delayedPoll.pollId);

            let users = {};
            await Promise.all(delayedPoll.reactions
                .flat()
                .filter((x, uidi) => delayedPoll.reactions.indexOf(x) !== uidi)
                .map(async uidToResolve => {
                    users[uidToResolve] = await client.users.fetch(uidToResolve);
                }));

            let toSend = {
                embed: {
                    title: `Zusammenfassung: ${message.embeds[0].title}`,
                    description: `${delayedPoll.reactions.map((x, index) => `${NUMBERS[index]} ${delayedPoll.reactionMap[index]} (${x.length}):
${x.map(uid => users[uid]).join("\n")}\n\n`).join("")}
`,
                    timestamp: moment.utc().format(),
                    author: {
                        name: `${message.embeds[0].author.name}`,
                        icon_url: message.embeds[0].author.iconURL
                    },
                    footer: {
                        text: `Gesamtabstimmungen: ${delayedPoll.reactions.map(x => x.length).reduce((a, b) => a + b)}`
                    }
                }
            };

            await channel.send(toSend);
            await Promise.all(message.reactions.cache.map(reaction => reaction.remove()));
            await message.react("✅");
            exports.delayedPolls.splice(exports.delayedPolls.indexOf(delayedPoll), 1);

            let messageData = await AdditionalMessageData.fromMessage(message);
            let {customData} = messageData;
            delete customData.delayedPollData;
            messageData.customData = customData;
            await messageData.save();
        }
    });
};

exports.description = `Erstellt eine Umfrage mit mehreren Antwortmöglichkeiten (standardmäßig mit Mehrfachauswahl) (maximal 10).
Usage: ${config.bot_settings.prefix.command_prefix}poll [Optionen?] [Hier die Frage] ; [Antwort 1] ; [Antwort 2] ; [...]
Optionen:
\t-c, --channel
\t\t\tSendet die Umfrage in den Umfragenchannel, um den Slowmode zu umgehen
\t-e, --extendable
\t\t\tErlaubt die Erweiterung der Antwortmöglichkeiten durch jeden User mit .extend als Reply
\t-s, --straw
\t\t\tStatt mehrerer Antworten kann nur eine Antwort gewählt werden
\t-d <T>, --delayed <T>
\t\t\tErgebnisse der Umfrage wird erst nach <T> Minuten angezeigt. (Noch) inkompatibel mit -e`;

export const applicationCommands: ApplicationCommandDefinition[] = [
    {
        handler,
        data: {
            name: "poll",
            description: "Erstellt eine Umfrage.",
            options: [
                {
                    name: "single",
                    type: "SUB_COMMAND",
                    description: "Erstellt eine Secure Decision aufgrund einer Fragestellung",
                    options: [
                        {
                            name: "question",
                            type: "STRING",
                            description: "Fragestellung",
                            required: true
                        }
                    ]
                },
                {
                    name: "multi",
                    type: "SUB_COMMAND",
                    description: "Erstellt eine Secure Decision anhand einer Auswahl",
                    options: [
                        {
                            name: "s1",
                            type: "STRING",
                            description: "Auswahlelement 1",
                            required: true
                        },
                        {
                            name: "s2",
                            type: "STRING",
                            description: "Auswahlelement 2",
                            required: true
                        },
                        {
                            name: "s3",
                            type: "STRING",
                            description: "Auswahlelement 3"
                        },
                        {
                            name: "s4",
                            type: "STRING",
                            description: "Auswahlelement 4"
                        },
                        {
                            name: "s5",
                            type: "STRING",
                            description: "Auswahlelement 35"
                        },
                        {
                            name: "s6",
                            type: "STRING",
                            description: "Auswahlelement 6"
                        },
                        {
                            name: "s7",
                            type: "STRING",
                            description: "Auswahlelement 7"
                        },
                        {
                            name: "s8",
                            type: "STRING",
                            description: "Auswahlelement 8"
                        },
                        {
                            name: "s9",
                            type: "STRING",
                            description: "Auswahlelement 9"
                        }
                    ]
                }
            ]
        }
    }
];
Code (vote.js)
"use strict";

const { MessageEmbed } = require("discord.js");
let moment = require("moment");
const { VoteData } = require("../storage/model/VoteData.js");

/**
 *
 * @param {import("discord.js").ButtonInteraction} interaction
 */
async function updateVotes(interaction) {
    /* eslint-disable */
    const { id, message, guild } = interaction;
    const votes = await VoteData.getVotes(id);
    const userNamesYes = votes.filter(v => v.vote).map(v => guild.members.cache.get(v.userId).user.username);
    const userNamesNo = votes.filter(v => !v.vote).map(v => guild.members.cache.get(v.userId).user.username);

    /**
     *  TODO: Edit the interaction reply embed - show votes
     *  If the interaction reply is not editable (or only within a timeslot),
     *  it may be necessary to create a separate message. However, this would
     *  require some additional steps as we need to keep track about the message
     *  ID.
     */
    await message.edit("test");
}

/**
 *
 * @param {import("discord.js").ButtonInteraction} interaction
 * @param {Function} callback
 */
async function handleYes(interaction, callback) {
    const interactionId = interaction.message.interaction?.id;
    const userId = interaction.member.id;
    await VoteData.setVote(interactionId, userId, true);
    interaction.reply({ content: "Hast mit Ja abgestimmt", ephemeral: true });
    updateVotes(interaction);
    return callback();
}

/**
 *
 * @param {import("discord.js").ButtonInteraction} interaction
 * @param {Function} callback
 */
async function handleNo(interaction, callback) {
    const interactionId = interaction.message.interaction?.id;
    const userId = interaction.member.id;
    await VoteData.setVote(interactionId, userId, false);
    interaction.reply({ content: "Hast mit Nein abgestimmt", ephemeral: true });
    updateVotes(interaction);
    return callback();
}

/**
 * @param {import("discord.js").CommandInteraction} interaction
 * @param {Function} callback
 */
async function handler(interaction, callback) {
    let question = interaction.options.get("question").value;
    if(!question.endsWith("?")) question += "?";

    let privacy = interaction.options.get("privacy").value;
    if(privacy !== "public") {
        interaction.reply({ content: "Geht noch nicht"});
        return callback();
    }

    interaction.reply({
        embeds: [
            new MessageEmbed()
                .setAuthor(`Abstimmung von ${interaction.member.user.username}`, interaction.member.user.displayAvatarURL())
                .setTitle(question)
                .setColor(0x206694)
                .setTimestamp(moment.utc().format())
        ],
        components: [
            {
                type: 1,
                components: [
                    {
                        type: 2,
                        label: "Ja!",
                        customID: "vote_yes",
                        style: 3,
                        emoji: {
                            id: null,
                            name: "👍"
                        }
                    },
                    {
                        type: 2,
                        label: "Nein!",
                        customID: "vote_no",
                        style: 4,
                        emoji: {
                            id: null,
                            name: "👎"
                        }
                    }
                ]
            }
        ]
    });

    return callback();
}

exports.description = "Erstellt ne Umfrage";

exports.applicationCommands = [
    {
        handler,
        data: {
            name: "vote",
            description: "Erstellt eine Umfrage",
            options: [
                {
                    name: "privacy",
                    type: "STRING",
                    description: "Privatsphäre der Umfrage",
                    required: true,
                    choices: [
                        {
                            name: "public",
                            value: "public"
                        },
                        {
                            name: "anonym",
                            value: "anonym"
                        }
                    ]
                },
                {
                    name: "question",
                    type: "STRING",
                    description: "Fragestellung",
                    required: true
                }
            ]
        },
        buttonHandler: {
            vote_yes: handleYes,
            vote_no: handleNo
        }
    }
];
Code (extend.js)
"use strict";

// =========================================== //
// = Copyright (c) NullDev & diewellenlaenge = //
// =========================================== //

/**
 * @typedef {import("discord.js").TextChannel} TC
 */

// Utils
let log = require("../utils/logger");
let config = require("../utils/configHandler").getConfig();

const NUMBERS = [
    ":one:",
    ":two:",
    ":three:",
    ":four:",
    ":five:",
    ":six:",
    ":seven:",
    ":eight:",
    ":nine:",
    ":keycap_ten:"
];

const EMOJI = [
    "1️⃣",
    "2️⃣",
    "3️⃣",
    "4️⃣",
    "5️⃣",
    "6️⃣",
    "7️⃣",
    "8️⃣",
    "9️⃣",
    "🔟"
];

/**
 * Extends an existing poll or strawpoll
 *
 * @param {import("discord.js").Client} client
 * @param {import("discord.js").Message} message
 * @param {Array} args
 * @param {Function} callback
 * @returns {Promise<Function>} callback
 */
exports.run = async(client, message, args, callback) => {
    if (!message.reference) return callback("Bruder schon mal was von der Replyfunktion gehört?");
    if (message.reference.guildID !== config.ids.guild_id || !message.reference.channelID) return callback("Bruder bleib mal hier auf'm Server.");

    let channel = client.guilds.cache.get(config.ids.guild_id).channels.cache.get(message.reference.channelID);

    if (!channel) return callback("Bruder der Channel existiert nicht? LOLWUT");

    let replyMessage = null;

    try {
        replyMessage = await /** @type {TC} */ (channel).messages.fetch(message.reference.messageID);
    }
    catch (err) {
        log.error(err);
        return callback("Bruder irgendwas stimmt nicht mit deinem Reply ¯\_(ツ)_/¯");
    }

    if (replyMessage.author.id !== client.user.id || replyMessage.embeds.length !== 1) return callback("Bruder das ist keine Umfrage ಠ╭╮ಠ");
    if (!replyMessage.embeds[0].author.name.startsWith("Umfrage") && !replyMessage.embeds[0].author.name.startsWith("Strawpoll")) return callback("Bruder das ist keine Umfrage ಠ╭╮ಠ");
    if (!replyMessage.editable) return callback("Bruder aus irgrndeinem Grund hat der Bot verkackt und kann die Umfrage nicht bearbeiten :<");
    if (replyMessage.embeds[0].color !== 3066993) return callback("Bruder die Umfrage ist nicht erweiterbar (ง'̀-'́)ง");

    let oldPollOptions = replyMessage.embeds[0].description.split("\n");

    if (oldPollOptions.length === 10) return callback("Bruder die Umfrage ist leider schon voll (⚆ ͜ʖ⚆)");

    for (let i = 0; i < oldPollOptions.length; ++i) {
        if (!oldPollOptions[i].startsWith(NUMBERS[i])) {
            return callback("Bruder das ist keine Umfrage ಠ╭╮ಠ");
        }
    }

    if (!args.length) return callback("Bruder da sind keine Antwortmöglichkeiten :c");

    let additionalPollOptions = args.join(" ").split(";").map(e => e.trim()).filter(e => e.replace(/\s/g, "") !== "");

    if (!additionalPollOptions.length) return callback("Bruder da sind keine Antwortmöglichkeiten :c");
    if (oldPollOptions.length + additionalPollOptions.length > 10) return callback(`Bruder die Umfrage hat schon ${oldPollOptions.length} Antwortmöglichkeiten und du wolltest noch ${additionalPollOptions.length} hinzufügen, dumm oder sowas?`);

    let originalAuthor = replyMessage.embeds[0].author.name.split(" ")[2];
    let authorNote = originalAuthor !== message.author.username ? ` (von ${message.author.username})` : "";

    let embed = replyMessage.embeds[0];
    embed.description += "\n";
    additionalPollOptions.forEach((e, i) => (embed.description += `${NUMBERS[oldPollOptions.length + i]} - ${e}${authorNote}\n`));

    if (oldPollOptions.length + additionalPollOptions.length === 10) {
        embed.color = null;
        delete embed.footer;
    }

    replyMessage.edit(undefined, embed).then(async msg => {
        for (let i in additionalPollOptions) await msg.react(EMOJI[oldPollOptions.length + Number(i)]);
    }).then(() => message.delete());

    return callback();
};

exports.description = `Nutzbar als Reply auf eine mit --extendable erstellte Umfrage, um eine/mehrere Antwortmöglichkeit/en hinzuzüfgen. Die Anzahl der bestehenden und neuen Antwortmöglichkeiten darf 10 nicht übersteigen.\nUsage: ${config.bot_settings.prefix.command_prefix}extend [Antwort 1] ; [...]`;
@diewellenlaenge diewellenlaenge added enhancement New feature or request spacktoberfest Label für Hacktoberfest, damit uns keiner auf die Nüsse geht labels Oct 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request spacktoberfest Label für Hacktoberfest, damit uns keiner auf die Nüsse geht
Projects
None yet
Development

No branches or pull requests

2 participants