Added the Poll module V1

This commit is contained in:
Mathias Wagner 2022-09-06 16:34:28 +02:00
parent 0817607697
commit d6c272cdaf
13 changed files with 737 additions and 0 deletions

3
SheepstarModules/PollV1/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Project exclude paths
/target/
.idea

View File

@ -0,0 +1,2 @@
# SheepstarModule-Poll
The official sheepstar poll module

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>xyz.sheepstar</groupId>
<artifactId>SheepstarModule-Poll</artifactId>
<version>pre1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>xyz.sheepstar</groupId>
<artifactId>SheepstarCore</artifactId>
<version>beta1.0.2</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,27 @@
package xyz.sheepstar.poll.action;
import xyz.sheepstar.poll.api.controller.PollController;
import xyz.sheepstar.poll.api.models.PollManager;
import xyz.sheepstar.poll.core.PollCore;
import xyz.sheepstar.util.action.RepeatedAction;
import java.time.LocalDateTime;
public class AutomaticCloseAction extends RepeatedAction {
private final PollManager pollManager = (PollManager) api.getDatabase().getTableFactory().getTable(PollManager.class);
private final PollController pollController = PollCore.getPollController();
@Override
public long time() {
return 5;
}
@Override
public void execute() {
pollManager.getExpirablePolls().forEach((pollID, dateTime) -> {
if (dateTime.compareTo(LocalDateTime.now()) <= 0)
pollController.endPoll(pollID, pollManager.getPollById(pollID).getGuild());
});
}
}

View File

@ -0,0 +1,163 @@
package xyz.sheepstar.poll.api.controller;
import net.dv8tion.jda.api.entities.Emoji;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.interactions.components.ButtonStyle;
import org.apache.commons.lang3.RandomStringUtils;
import xyz.sheepstar.poll.api.entities.Poll;
import xyz.sheepstar.poll.api.models.PollAnswerManager;
import xyz.sheepstar.poll.api.models.PollManager;
import xyz.sheepstar.poll.commands.ClosePollCommand;
import xyz.sheepstar.util.bot.builder.message.DefaultEmbedBuilder;
import xyz.sheepstar.util.bot.builder.message.DefaultResponseBuilder;
import xyz.sheepstar.util.bot.builder.message.MessageType;
import xyz.sheepstar.util.bot.listener.ListenerBasics;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
public class PollController extends ListenerBasics {
private PollManager pollManager = (PollManager) table(PollManager.class);
private PollAnswerManager pollAnswerManager = (PollAnswerManager) table(PollAnswerManager.class);
/**
* Gets the number emoji by number
*
* @param number The number the emoji should display
* @return the number as emoji
*/
private Emoji getEmojiByNumber(int number) {
return Emoji.fromUnicode(number + "️⃣");
}
/**
* Gets the 'started'-message of the poll
*
* @param pollID The id of the poll (used for the end button)
* @param date The date when the poll should expire
* @param title The title of the poll
* @param description The description of the poll
* @param answers All answers available in this poll
* @param guild The guild which created the poll
* @param closeButton Should the close button be displayed?
* @param multipleChoices Are multiple choices allowed?
* @return the final message
*/
public Message getStartedPollMessage(String pollID, Date date, String title, String description, ArrayList<String> answers, Guild guild,
boolean closeButton, boolean multipleChoices) {
DefaultResponseBuilder response = DefaultResponseBuilder.createSimple(guild, ClosePollCommand.class).withColor(MessageType.PRIMARY)
.withTitle(translate("poll.poll_started", guild));
response.addField(":label: " + translate("poll.object.title", guild), title);
if (description != null)
response.addField(":clipboard: " + translate("poll.object.description", guild), description);
ArrayList<String> subFields = new ArrayList<>();
for (int i = 0; i < answers.size(); i++)
subFields.add(getEmojiByNumber(i + 1).getAsMention() + " - " + answers.get(i));
response.addField(":ballot_box: " + translate("poll.object." + (multipleChoices ? "multiple_" : "") + "choices", guild) + "", subFields);
if (date != null)
response.addField(String.format("\n:alarm_clock: %s <t:%s:R>", translate("poll.ends", guild), date.getTime() / 1000));
if (closeButton)
response.addButton(ButtonStyle.DANGER, "end#" + pollID, date == null ? translate("poll.end_poll", guild) : translate("poll.end_now", guild));
return response.build();
}
/**
* Gets the 'ended'-message of the poll
*
* @param poll The poll you want to get the message from
* @return The final 'ended'-message
*/
public Message getEndedPollMessage(Poll poll) {
DefaultEmbedBuilder embedBuilder = new DefaultEmbedBuilder(MessageType.PRIMARY).setTitle(translate("poll.poll_ended", poll.getGuild()));
StringBuilder builder = new StringBuilder();
boolean isTimeMessage = false;
for (String line : poll.getMessage().getEmbeds().get(0).getDescription().split("\n")) {
if (line.startsWith(":alarm_clock:")) isTimeMessage = true;
if (!line.startsWith(":alarm_clock:")) builder.append(line).append("\n");
}
embedBuilder.setDescription((!isTimeMessage ? builder + "\n" : builder.substring(0, builder.length() - 3)));
HashMap<String, Integer> reaction_counts = new HashMap<>();
poll.getMessage().getReactions().forEach(reaction -> {
if (reaction.getReactionEmote().isEmoji())
reaction_counts.put(reaction.getReactionEmote().getEmoji(), reaction.getCount() - 1);
});
ArrayList<String> subFields = new ArrayList<>();
for (int i = 0; i < poll.getAnswers().size(); i++)
subFields.add(getEmojiByNumber(i + 1).getAsMention() + " - " + reaction_counts.getOrDefault(getEmojiByNumber(i + 1).getAsMention(), 0));
embedBuilder.addBField(":abacus: " + translate("poll.object.results", poll.getGuild()), subFields.toArray(new String[0]));
return embedBuilder.toMessage();
}
/**
* Creates a new poll
*
* @param channel The channel the poll should be created in
* @param date The date when the poll should expire
* @param title The title of the poll
* @param description The description of the poll
* @param answers All answers available in this poll
* @param closeButton Should the close button be displayed?
* @param multipleChoices Are multiple choices allowed?
* @return the id of the created poll
*/
public String createPoll(TextChannel channel, Date date, String title, String description, ArrayList<String> answers, boolean closeButton, boolean multipleChoices) {
String pollID = RandomStringUtils.randomAlphanumeric(10);
pollAnswerManager.addAnswers(pollID, answers);
channel.sendMessage(getStartedPollMessage(pollID, date, title, description, answers, channel.getGuild(), closeButton, multipleChoices)).queue(message -> {
pollManager.createPoll(pollID, message, date);
for (int i = 0; i < answers.size(); i++)
message.addReaction(getEmojiByNumber(i + 1).getAsMention()).queue();
});
return pollID;
}
/**
* Ends the provided poll
*
* @param pollID The poll you want to end
* @param guild The guild which created the poll
* @return <code>true</code> if the poll has been ended successfully, otherwise <code>false</code>
*/
public boolean endPoll(String pollID, Guild guild) {
if (!pollManager.isCreated(pollID, guild)) return false;
try {
Poll poll = pollManager.getPollById(pollID);
poll.getMessage().editMessage(getEndedPollMessage(poll)).queue();
poll.getMessage().clearReactions().queue();
} catch (Exception e) {
return false;
}
pollManager.deletePoll(pollID, guild);
pollAnswerManager.deleteAnswers(pollID);
return true;
}
}

View File

@ -0,0 +1,93 @@
package xyz.sheepstar.poll.api.entities;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import xyz.sheepstar.core.SheepstarCore;
import xyz.sheepstar.poll.api.models.PollAnswerManager;
import java.time.LocalDateTime;
import java.util.ArrayList;
public class Poll {
private PollAnswerManager pollAnswerManager = (PollAnswerManager) SheepstarCore
.getSheepstar().getDatabase().getTableFactory().getTable(PollAnswerManager.class);
private String pollID;
private Guild guild;
private TextChannel channel;
private Message message;
private LocalDateTime expiryDate;
private ArrayList<String> answers;
/**
* Basic constructor of the {@link Poll} entity
*
* @param pollID The id of the poll
* @param message The poll message created by the bot
* @param expiryDate The date when the poll should expire
*/
public Poll(String pollID, Message message, LocalDateTime expiryDate) {
this.pollID = pollID;
this.message = message;
this.guild = message.getGuild();
this.channel = message.getTextChannel();
this.expiryDate = expiryDate;
this.answers = pollAnswerManager.getAnswers(pollID);
}
/**
* Gets the poll id
*
* @return the poll id
*/
public String getPollID() {
return pollID;
}
/**
* Gets the guild
*
* @return the guild
*/
public Guild getGuild() {
return guild;
}
/**
* Gets the channel
*
* @return the channel
*/
public TextChannel getChannel() {
return channel;
}
/**
* Gets the message
*
* @return the message
*/
public Message getMessage() {
return message;
}
/**
* Gets the date when the poll should expire
*
* @return the expiry date
*/
public LocalDateTime getExpiryDate() {
return expiryDate;
}
/**
* Gets all answers
*
* @return all answers
*/
public ArrayList<String> getAnswers() {
return answers;
}
}

View File

@ -0,0 +1,59 @@
package xyz.sheepstar.poll.api.models;
import xyz.sheepstar.util.sql.SheepManager;
import java.util.ArrayList;
public class PollAnswerManager extends SheepManager {
@Override
protected String tableName() {
return "poll_poll_answers";
}
@Override
protected void tableFields() {
custom("poll_id").add();
custom("answer").add();
}
/**
* Adds a single answer to the poll answers
*
* @param pollID The id of the poll you want to add the answer to
* @param answer The answer you want to add
*/
public void addAnswer(String pollID, String answer) {
insert().value("poll_id", pollID).value("answer", answer).execute();
}
/**
* Adds multiple answers to the poll answers
*
* @param pollID The id of the poll you want to add the answer to
* @param answers The answers you want to add
*/
public void addAnswers(String pollID, ArrayList<String> answers) {
answers.forEach(answer -> addAnswer(pollID, answer));
}
/**
* Deletes all answers from a poll
*
* @param pollID The id of the poll
*/
public void deleteAnswers(String pollID) {
delete().where("poll_id", pollID).execute();
}
/**
* Gets all answers from the poll
*
* @param pollID The id of the poll you want to get the answers from
* @return all answers from the poll
*/
public ArrayList<String> getAnswers(String pollID) {
return select().where("poll_id", pollID).getResult().getList("answer");
}
}

View File

@ -0,0 +1,115 @@
package xyz.sheepstar.poll.api.models;
import de.gnmyt.sqltoolkit.types.SQLType;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import xyz.sheepstar.poll.api.entities.Poll;
import xyz.sheepstar.util.sql.SheepManager;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
public class PollManager extends SheepManager {
@Override
protected String tableName() {
return "poll_polls";
}
@Override
protected void tableFields() {
custom("poll_id").add();
custom("guild_id").add();
custom("channel_id").add();
custom("message_id").add();
custom("expiry").type(SQLType.DATETIME).allowNull(true).add();
}
/**
* Checks if the provided poll has been already created
*
* @param pollID The id of the poll you want to check
* @param guild The guild in which the poll has been created
* @return <code>true</code> if the provided poll has been already created, otherwise <code>false</code>
*/
public boolean isCreated(String pollID, Guild guild) {
return select().where("guild_id", guild.getId()).where("poll_id", pollID).getResult().exists();
}
/**
* Checks if the provided message is a poll
*
* @param message The message you want to check
* @return <code>true</code> if the message is a poll, otherwise <code>false</code>
*/
public boolean isPollMessage(Message message) {
return select().where("message_id", message.getId()).getResult().exists();
}
/**
* Gets the id from the poll by a message
*
* @param message The message you want to get the poll id from
* @return the id of the poll
*/
public String getIDByMessage(Message message) {
return select().where("message_id", message.getId()).getResult().getString("poll_id");
}
/**
* Creates a new poll for you
*
* @param pollID The id the poll should have
* @param message The already sent message of the poll (to edit if finished)
* @param expiry The date when the poll should expire, <code>NULL</code> if it should not close automatically
*/
public void createPoll(String pollID, Message message, Date expiry) {
if (isCreated(pollID, message.getGuild())) return;
insert()
.value("poll_id", pollID)
.value("guild_id", message.getGuild().getId())
.value("channel_id", message.getChannel().getId())
.value("message_id", message.getId())
.value("expiry", expiry)
.execute();
}
/**
* Deletes a poll from the table
*
* @param pollID The id of the poll you want to delete
* @param guild The id of the guild the poll was created on
*/
public void deletePoll(String pollID, Guild guild) {
if (!isCreated(pollID, guild)) return;
delete().where("poll_id", pollID).where("guild_id", guild.getId()).execute();
}
/**
* Gets all polls that <b>can</b> expire
*
* @return all polls that can expire
*/
public HashMap<String, LocalDateTime> getExpirablePolls() {
HashMap<String, LocalDateTime> expirable_polls = new HashMap<>();
select().getResult().getList().forEach(entry -> {
if (entry.get("expiry") != null)
expirable_polls.put((String) entry.get("poll_id"), (LocalDateTime) entry.get("expiry"));
});
return expirable_polls;
}
/**
* Gets a specific poll by id
*
* @param pollID The id of the poll you want to get
* @return the poll
*/
public Poll getPollById(String pollID) {
return new Poll(pollID, api.getJDA().getTextChannelById(select().where("poll_id", pollID)
.getResult().getString("channel_id")).retrieveMessageById(select().where("poll_id", pollID).getResult().getString("message_id")).complete(),
(LocalDateTime) select().where("poll_id", pollID).getResult().getObject("expiry"));
}
}

View File

@ -0,0 +1,35 @@
package xyz.sheepstar.poll.commands;
import xyz.sheepstar.poll.api.controller.PollController;
import xyz.sheepstar.poll.core.PollCore;
import xyz.sheepstar.util.bot.command.Arguments;
import xyz.sheepstar.util.bot.command.GuildCommand;
import xyz.sheepstar.util.bot.command.GuildEventController;
import xyz.sheepstar.util.bot.command.PublicCommandException;
import xyz.sheepstar.util.bot.command.annotations.CommandMeta;
import xyz.sheepstar.util.bot.permission.PermissionNode;
@CommandMeta(aliases = "poll", subAliases = "close", description = "Closes a specific poll", permission = PermissionNode.ADMINISTRATOR)
public class ClosePollCommand extends GuildCommand {
@Override
public void usage() {
usage("poll_id", "The id of the poll you want to close").required(true).add();
}
private PollController pollController = PollCore.getPollController();
@Override
public void execute(GuildEventController event, Arguments args) throws Exception {
if (pollController.endPoll(args.getString("poll_id"), event.getGuild())) {
event.success("poll.ended_successfully");
} else throw new PublicCommandException("poll.not_found");
}
@Override
public void buttonClick(GuildEventController event, String id) throws Exception {
if (!id.startsWith("end#")) return;
pollController.endPoll(id.split("#")[1], event.getGuild());
}
}

View File

@ -0,0 +1,78 @@
package xyz.sheepstar.poll.commands;
import net.dv8tion.jda.api.entities.ChannelType;
import net.dv8tion.jda.api.entities.GuildChannel;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import xyz.sheepstar.poll.api.controller.PollController;
import xyz.sheepstar.poll.core.PollCore;
import xyz.sheepstar.util.bot.command.Arguments;
import xyz.sheepstar.util.bot.command.GuildCommand;
import xyz.sheepstar.util.bot.command.GuildEventController;
import xyz.sheepstar.util.bot.command.PublicCommandException;
import xyz.sheepstar.util.bot.command.annotations.CommandMeta;
import xyz.sheepstar.util.bot.permission.PermissionNode;
import java.util.ArrayList;
import java.util.Date;
@CommandMeta(aliases = "poll", subAliases = "create", permission = PermissionNode.ADMINISTRATOR, description = "Creates a poll")
public class CreatePollCommand extends GuildCommand {
private PollController pollController = PollCore.getPollController();
@Override
public void usage() {
usage("title", "The title of the poll").required(true).add();
usage("choice_1", "The first choice").required(true).add();
usage("choice_2", "The second choice").required(true).add();
usage("description", "The description of the poll").add();
usage("time", "When should the poll automatically be dissolved").add();
usage(OptionType.BOOLEAN, "close_button", "Should the poll show a close button? Otherwise you get the id to close the poll").add();
usage(OptionType.BOOLEAN, "multiple_choices", "Allows the user to click on multiple options").add();
usage(OptionType.CHANNEL, "channel", "The channel in which the message should be sent (for example if you have an poll channel)").add();
// All optional choices
usage("choice_3", "The third choice").add();
usage("choice_4", "The fourth choice").add();
usage("choice_5", "The fifth choice").add();
usage("choice_6", "The 6th choice").add();
usage("choice_7", "The 7th choice").add();
usage("choice_8", "The 8th choice").add();
usage("choice_9", "The 9th choice").add();
}
@Override
public void execute(GuildEventController event, Arguments args) throws Exception {
GuildChannel channel = args.exists("channel") ? args.getChannel("channel") : event.getChannel();
Date date = args.exists("time") ? getTimeFromString(args.getString("time")) : null;
boolean closeButton = !args.exists("close_button") || args.getBoolean("close_button");
boolean multipleChoices = args.exists("multiple_choices") && args.getBoolean("multiple_choices");
if (channel.getType() != ChannelType.TEXT)
throw new PublicCommandException("poll.error.channel.must_be_text");
ArrayList<String> answers = new ArrayList<>();
for (int i = 1; i < 10; i++)
if (args.exists("choice_" + i)) answers.add(args.getString("choice_" + i));
String pollID = pollController.createPoll((TextChannel) channel, date, args.getString("title"), args.getString("description"), answers, closeButton, multipleChoices);
event.success("poll.poll_created", channel.getAsMention(), pollID);
}
}

View File

@ -0,0 +1,70 @@
package xyz.sheepstar.poll.core;
import xyz.sheepstar.poll.action.AutomaticCloseAction;
import xyz.sheepstar.poll.api.controller.PollController;
import xyz.sheepstar.poll.api.models.PollAnswerManager;
import xyz.sheepstar.poll.api.models.PollManager;
import xyz.sheepstar.poll.commands.ClosePollCommand;
import xyz.sheepstar.poll.commands.CreatePollCommand;
import xyz.sheepstar.poll.listener.MultipleReactionListener;
import xyz.sheepstar.util.bot.manager.ImportManager;
import xyz.sheepstar.util.module.SheepstarModule;
public class PollCore extends SheepstarModule {
private static PollController pollController;
private ImportManager importManager;
@Override
public void onEnable() {
importManager = new ImportManager(getAPI(), "poll");
registerManagers();
pollController = new PollController();
registerCommands();
registerActions();
registerListeners();
}
/**
* Registers all managers of the module
*/
public void registerManagers() {
registerTable(new PollAnswerManager());
registerTable(new PollManager());
}
/**
* Registers all commands of the module
*/
public void registerCommands() {
importManager.registerCommand(new CreatePollCommand());
importManager.registerCommand(new ClosePollCommand());
}
/**
* Registers all listeners of the module
*/
public void registerListeners() {
importManager.registerListener(new MultipleReactionListener());
}
/**
* Registers all actions of the module
*/
public void registerActions() {
new AutomaticCloseAction().start();
}
/**
* Gets the poll controller
*
* @return the poll controller
*/
public static PollController getPollController() {
return pollController;
}
}

View File

@ -0,0 +1,65 @@
package xyz.sheepstar.poll.listener;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
import xyz.sheepstar.poll.api.models.PollAnswerManager;
import xyz.sheepstar.poll.api.models.PollManager;
import xyz.sheepstar.util.bot.listener.GuildListener;
import java.util.concurrent.atomic.AtomicInteger;
public class MultipleReactionListener extends GuildListener {
private PollManager pollManager = (PollManager) table(PollManager.class);
private PollAnswerManager pollAnswerManager = (PollAnswerManager) table(PollAnswerManager.class);
/**
* Checks if the provided emoji is valid
*
* @param emoji The emoji you want to check
* @param message The message to get the size of the answers
* @return <code>true</code> if the emoji is valid, otherwise <code>false</code>
*/
public boolean isValidEmote(String emoji, Message message) {
for (int i = 0; i < pollAnswerManager.getAnswers(pollManager.getIDByMessage(message)).size(); i++) {
if (emoji.contains(String.valueOf(i + 1))) return true;
}
return false;
}
@Override
public void onGuildMessageReactionAdd(GuildMessageReactionAddEvent event) {
event.retrieveMessage().queue(message -> {
if (event.getUser().getId().equals(jda.getSelfUser().getId())) return;
if (!message.getAuthor().getId().equals(jda.getSelfUser().getId())) return;
if (!pollManager.isPollMessage(message)) return;
// TODO: Migrate to id instead of message
if (!event.getReactionEmote().isEmoji()) {
event.getReaction().removeReaction(event.getUser()).queue();
return;
}
if (!isValidEmote(event.getReactionEmote().getEmoji(), message)) {
event.getReaction().removeReaction(event.getUser()).queue();
return;
}
String[] embedLines = message.getEmbeds().get(0).getDescription().split("\n");
for (String line : embedLines)
if (line.startsWith("**:ballot_box:") && line.contains("(")) return;
AtomicInteger reactAmount = new AtomicInteger();
message.getReactions().forEach(reaction -> reaction.retrieveUsers().queue(users -> users.forEach(user -> {
if (user.getId().equals(event.getUser().getId())) reactAmount.getAndIncrement();
if (reactAmount.get() > 1) {
event.getReaction().removeReaction(event.getUser()).queue();
return;
}
})));
});
}
}

View File

@ -0,0 +1,3 @@
main: xyz.sheepstar.poll.core.PollCore
name: poll
author: Mathias Wagner