107 Commits

Author SHA1 Message Date
bea f934c7d7f4 Update instructions order when registering listeners
continuous-integration/drone/push Build is passing
2022-12-28 05:36:58 +01:00
bea 61985ff193 Update a small comment 2022-12-28 05:36:54 +01:00
bea d07664ea04 Update 'README.MD' 2022-12-28 05:36:50 +01:00
bea 26f071aaf5 Update 'README.MD' 2022-12-28 05:36:38 +01:00
bea f0c55f7bf8 Bump version to 0.5.16
continuous-integration/drone/push Build is passing
2022-12-26 04:11:01 +01:00
bea 791d314da4 Implement ban, kick, timeout slash commands
continuous-integration/drone/push Build is passing
2022-12-26 04:10:17 +01:00
bea 81d3aebb7f Make trivia command defer reply
continuous-integration/drone/push Build is passing
2022-12-26 03:51:43 +01:00
bea ffb5ef7181 Rework time parsing utils
continuous-integration/drone/push Build is passing
2022-12-26 03:34:26 +01:00
bea 51e11e8445 Implement basic timeout command
continuous-integration/drone/push Build is failing
2022-12-26 03:34:11 +01:00
bea 00c61968b8 Implement basic ban command
continuous-integration/drone/push Build is passing
2022-12-26 02:40:25 +01:00
bea e2fda4c7cd Implement basic kick command
continuous-integration/drone/push Build is passing
2022-12-26 00:22:32 +01:00
bea 11d4b7fa56 Add bot age info
continuous-integration/drone/push Build is passing
2022-12-25 02:12:39 +01:00
bea f7ef27066d Bump version to 0.5.15
continuous-integration/drone/push Build is passing
2022-12-25 01:49:43 +01:00
bea beae316bb3 Implement profile banner grabber command
continuous-integration/drone/push Build is passing
2022-12-25 01:48:31 +01:00
bea b02892d60b Fix minetest's name in random statuses
continuous-integration/drone/push Build is passing
2022-12-24 15:20:39 +01:00
bea 8b2197b4f6 Fix missing JSON dependency
continuous-integration/drone/push Build is passing
2022-12-24 14:48:17 +01:00
bea e65ec54fd7 Mitigate potential RCE from SnakeYaml (CVE-2022-1471)
continuous-integration/drone/push Build is failing
This vulnerability is very unlikely to ever happen, since the only way to modify the YAML file is to edit it yourself, and it would be useless for a bot owner to RCE their own bot. No other person can edit the configuration file remotely (eg. with bot commands), so realistically, this could not happen.
2022-12-24 14:42:01 +01:00
bea cd1a50a6d1 Make trivia support slash commands too
continuous-integration/drone/push Build is passing
2022-12-21 23:31:12 +01:00
bea ad8078809b Handle trivia edge cases without hanging
continuous-integration/drone/push Build is passing
2022-12-21 20:27:56 +01:00
bea 5488ec567e Bump version to 0.5.14
continuous-integration/drone/push Build is passing
2022-12-21 17:59:40 +01:00
bea 42cb72fd3d Implement trivia welcome screen with category picker
continuous-integration/drone/push Build is passing
2022-12-21 17:59:25 +01:00
bea 71904f4243 Bump version to 0.5.13
continuous-integration/drone/push Build is passing
2022-12-21 16:40:41 +01:00
bea 69bd1a5652 Make trivia have a functional scoreboard
continuous-integration/drone/push Build is passing
2022-12-21 16:40:17 +01:00
bea d188eae1e2 Make trivia functional
continuous-integration/drone/push Build is passing
2022-12-21 14:24:07 +01:00
bea 336f8364c7 Add emojis to trivia buttons
continuous-integration/drone/push Build is passing
2022-12-21 04:37:32 +01:00
bea acb2ee21c2 Make trivia announce correct answer
continuous-integration/drone/push Build is passing
2022-12-21 04:26:36 +01:00
bea c02cc8c7df Raise trivia timeout to 15s
continuous-integration/drone/push Build is passing
2022-12-21 04:11:05 +01:00
bea 8d0d181ad9 Make trivia loop through all questions
continuous-integration/drone/push Build is passing
2022-12-21 04:09:27 +01:00
bea 7dce206a01 Disable trivia in dms
continuous-integration/drone/push Build is passing
2022-12-21 03:50:23 +01:00
bea 702ed65a12 Raise interaction expiration time to 30s
continuous-integration/drone/push Build is passing
2022-12-21 03:43:55 +01:00
bea 53fd3dc81d Make trivia functional for a single question
continuous-integration/drone/push Build is passing
2022-12-21 03:40:13 +01:00
bea c3354b9976 Only fetch multiple-answer trivia for now
continuous-integration/drone/push Build is passing
2022-12-21 02:54:07 +01:00
bea ae1101e93f Fix build errors
continuous-integration/drone/push Build is passing
2022-12-21 02:52:17 +01:00
bea 3f67777659 Remove unneeded methods from MessageResponse
continuous-integration/drone/push Build is failing
2022-12-21 02:51:23 +01:00
bea c63bafc88e Start implementing trivia command
continuous-integration/drone/push Build is passing
2022-12-21 02:50:22 +01:00
bea 6c857e2f9a Remove double space on urban footer
continuous-integration/drone/push Build is passing
2022-12-20 23:37:37 +01:00
bea 6c33581dc4 Add emojis to urban dictionary
continuous-integration/drone/push Build is passing
2022-12-20 23:30:40 +01:00
bea 931d6efaef Convert message response to immutable record
continuous-integration/drone/push Build is passing
2022-12-20 23:07:01 +01:00
bea e3e8b469ba Bump version to 0.5.12
continuous-integration/drone/push Build is passing
2022-12-20 23:03:34 +01:00
bea 54022221a0 Make dice roll support slash commands too
continuous-integration/drone/push Build is passing
2022-12-20 23:03:21 +01:00
bea 58c4412f75 Add a MessageResponse class for mixed-type content
continuous-integration/drone/push Build is failing
2022-12-20 23:03:07 +01:00
bea 0bd63e76bd Rename method
continuous-integration/drone/push Build is passing
2022-12-20 22:49:20 +01:00
bea d49fe3ee15 Cache love calculator results in RAM
continuous-integration/drone/push Build is passing
2022-12-20 22:48:29 +01:00
bea 5e48652587 Make love calculator also support slash commands
continuous-integration/drone/push Build is passing
2022-12-20 22:32:40 +01:00
bea 6ffe10e4c8 Optimize imports 2022-12-20 22:24:34 +01:00
bea 0f54fe856e Increase randomness by updating the random's seed every minute
continuous-integration/drone/push Build is passing
2022-12-20 22:15:52 +01:00
bea 5a7f884703 Bump version to 0.5.11 2022-12-20 22:08:58 +01:00
bea e5c5993fb2 Make random statuses update automatically
continuous-integration/drone/push Build is passing
2022-12-20 22:08:05 +01:00
bea d331c48ced Fix invite command being categorized as fun
continuous-integration/drone/push Build is passing
2022-12-20 18:04:37 +01:00
bea 84ff5a752e Improve help command title
continuous-integration/drone/push Build is passing
2022-12-20 18:03:02 +01:00
bea 4f408fb5f9 Make permissions bold instead of code-wrapped
continuous-integration/drone/push Build is passing
2022-12-20 17:58:14 +01:00
bea 1384259187 Bump version to 0.5.10
continuous-integration/drone/push Build is passing
2022-12-20 17:55:43 +01:00
bea 82698ec5fe Fix nothing being rolles if no arg was specified
continuous-integration/drone/push Build is passing
2022-12-20 17:55:30 +01:00
bea 0762068465 Make help command use descriptions and usages
continuous-integration/drone/push Build is passing
2022-12-20 17:51:28 +01:00
bea cc9aee3441 Fix spacing 2022-12-20 17:23:52 +01:00
bea 86c7c30d8f Improve default responses 2022-12-20 17:23:20 +01:00
bea 19e3cde7e6 Bump version to 0.5.9
continuous-integration/drone/push Build is passing
2022-12-20 17:18:39 +01:00
bea 72f9bb4eb5 Make command category not null
continuous-integration/drone/push Build is passing
2022-12-20 17:18:24 +01:00
bea 407ca279f5 Implement alias command 2022-12-20 17:18:09 +01:00
bea 9eefa4b958 Implement alias command 2022-12-20 17:17:13 +01:00
bea 9278b485d9 Add help command and command categories
continuous-integration/drone/push Build is passing
2022-12-20 17:06:18 +01:00
bea 5a2205e567 Remove deprecated and unused private method
continuous-integration/drone/push Build is passing
2022-12-20 15:15:03 +01:00
bea 3212ceb03c Throw exception in case of serialization issue
continuous-integration/drone/push Build is passing
2022-12-20 15:13:21 +01:00
bea a80b2cc5a9 Make serialization util class
continuous-integration/drone/push Build is passing
2022-12-20 15:11:44 +01:00
bea d087de1d01 Use enum instead of boolean for page switching
continuous-integration/drone/push Build is passing
This is useless but looks better
2022-12-20 15:09:35 +01:00
bea 99fc980e00 Remove duplication
continuous-integration/drone/push Build is passing
2022-12-20 15:04:39 +01:00
bea 02627ab732 Add javadoc comment
continuous-integration/drone/push Build is passing
2022-12-20 14:53:58 +01:00
bea dead16f338 Merge two classes
continuous-integration/drone/push Build is passing
2022-12-20 14:51:29 +01:00
bea 2f51f9d40c Optimize imports
continuous-integration/drone/push Build is passing
2022-12-20 14:50:09 +01:00
bea 5f5fc8d3a8 Remove duplicated method
continuous-integration/drone/push Build is passing
2022-12-20 14:49:44 +01:00
bea 4c98182da7 Fix small emoji translation issue
continuous-integration/drone/push Build is passing
2022-12-20 03:44:00 +01:00
bea 9e57a3a426 Optimize imports
continuous-integration/drone/push Build is passing
2022-12-20 03:17:32 +01:00
bea 2bf08c27b7 Make urban command support slash too
continuous-integration/drone/push Build is passing
2022-12-20 02:17:27 +01:00
bea b085efeccb Allow sender to delete their own urban command results
continuous-integration/drone/push Build is passing
2022-12-20 00:59:26 +01:00
bea d8604c7ae5 Fix urban dictionary term not getting parsed correctly for url
continuous-integration/drone/push Build is passing
2022-12-20 00:52:31 +01:00
bea e83d7de7f5 Bump version to 0.5.8
continuous-integration/drone/push Build is passing
2022-12-20 00:35:08 +01:00
bea dfa25e54f3 Make urban command support multiple entries
continuous-integration/drone/push Build is passing
2022-12-20 00:34:49 +01:00
bea d1dc71dde9 Improve urban dictionary parsing
continuous-integration/drone/push Build is passing
2022-12-19 22:53:25 +01:00
bea 6fcd3b4cdf Improve urban dictionary parsing
continuous-integration/drone/push Build is passing
2022-12-19 22:45:02 +01:00
bea deb7d83e64 Keep newlines in urban dictionary parser
continuous-integration/drone/push Build is passing
2022-12-19 21:58:57 +01:00
bea 010a25fd66 Bump version to 0.5.7
continuous-integration/drone/push Build is passing
2022-12-19 21:43:07 +01:00
bea 7c2530c88b Implement urban dictionary lookup command
continuous-integration/drone/push Build is passing
2022-12-19 21:42:55 +01:00
bea 20665f4862 Change a magic ball response
continuous-integration/drone/push Build is passing
2022-12-19 21:03:20 +01:00
bea ecfa3cded8 Make it send a message instead of responding to 8ball
continuous-integration/drone/push Build is passing
2022-12-19 21:00:25 +01:00
bea 90e0c4ddf9 Bump version to 0.5.6
continuous-integration/drone/push Build is passing
2022-12-19 20:39:16 +01:00
bea fd9fe4ead6 Add basic love calculator message command
continuous-integration/drone/push Build is passing
2022-12-19 20:38:32 +01:00
bea 6cdd44da29 Bump version to 0.5.5
continuous-integration/drone/push Build is passing
2022-12-19 18:37:38 +01:00
bea 3dd30a3a89 Make magicball support slash commands too
continuous-integration/drone/push Build is passing
2022-12-19 18:37:17 +01:00
bea 5c8bad2b02 Improve magic ball answers
continuous-integration/drone/push Build is passing
2022-12-19 18:28:21 +01:00
bea b23bc30fc0 Implement magic ball message command
continuous-integration/drone/push Build is passing
2022-12-19 18:24:29 +01:00
bea 018e24034f Bump version to 0.5.4
continuous-integration/drone/push Build is passing
2022-12-19 17:32:18 +01:00
Lorenzo Dellacà 1a19a9ea06 Improve diceroll looks, implement limits to avoid abuse
continuous-integration/drone/push Build is passing
2022-12-19 17:31:28 +01:00
Lorenzo Dellacà 495f164552 Ignore bots interacting with hideko
continuous-integration/drone/push Build is passing
2022-12-19 16:54:15 +01:00
Lorenzo Dellacà fd100649a7 Remove unneeded todo 2022-12-19 16:48:20 +01:00
Lorenzo Dellacà b3990ff04f Make clear command also delete the sender's message
continuous-integration/drone/push Build is passing
2022-12-19 16:47:49 +01:00
Lorenzo Dellacà f5238ced89 Bump version to 0.5.3
continuous-integration/drone/push Build is passing
2022-12-19 01:41:04 +01:00
Lorenzo Dellacà f0ee565185 Implement basic functional diceroll command
continuous-integration/drone/push Build is passing
2022-12-19 01:36:43 +01:00
Lorenzo Dellacà a21d179308 Fix command label being passed as arg in case of no args
continuous-integration/drone/push Build is passing
2022-12-19 00:22:51 +01:00
Lorenzo Dellacà 36ad728bbc Fallback to 0 instead of 1
continuous-integration/drone/push Build is passing
2022-12-19 00:14:30 +01:00
Lorenzo Dellacà 1a6fe6465c Fix console error when int parsing fails in clear message
continuous-integration/drone/push Build is passing
2022-12-19 00:13:14 +01:00
Lorenzo Dellacà f0004dc555 Re-register accidentally removed invite command
continuous-integration/drone/push Build is passing
2022-12-19 00:10:15 +01:00
Lorenzo Dellacà 8ddf0ab80d Bump JDA version to more stable beta
continuous-integration/drone/push Build is passing
2022-12-19 00:07:02 +01:00
Lorenzo Dellacà 660e18d1f4 Bump version to 0.5.2
continuous-integration/drone/push Build is passing
2022-12-19 00:05:49 +01:00
Lorenzo Dellacà db943f7e05 Fix messages with newlines not being handled for commands
continuous-integration/drone/push Build is passing
2022-12-19 00:05:36 +01:00
Lorenzo Dellacà cb49bda84a Make say support both slash and message commands 2022-12-19 00:05:13 +01:00
Lorenzo Dellacà b318b9f22b Bump version to 0.5.1
continuous-integration/drone/push Build is passing
2022-12-18 23:49:00 +01:00
Lorenzo Dellacà 1447f8c177 Make avatar support both slash and message commands
continuous-integration/drone/push Build is passing
2022-12-18 23:47:54 +01:00
71 changed files with 4283 additions and 358 deletions
+7 -2
View File
@@ -12,7 +12,7 @@ make the bot change its behavior.
Additionally available parameters are: Additionally available parameters are:
- **verbose**: log every message that the bot receives, plus additional debugging messages. Very spammy and performance heavy. - **verbose**: log every message that the bot receives, plus additional debugging messages. Very spammy and performance heavy.
- **refresh**: force refresh the bot's commands. - **refresh**: force refresh the slash commands. This is useful in case there was a simple update to a command that did not drastically change it, so no changes are found at bootup (eg: fixing a typo in the command description).
*Note: Java 16 or later is required.* *Note: Java 16 or later is required.*
@@ -24,4 +24,9 @@ Edit the configuration file and set all values according to your needs.
Save the file and start the bot again. If there are no issues, everything will load and it will print an Save the file and start the bot again. If there are no issues, everything will load and it will print an
invite-link in your console. Click on the link to add your bot to any server with the correct permissions invite-link in your console. Click on the link to add your bot to any server with the correct permissions
already set-up. already set-up. The bot supports both slash commands and message commands, with prefix `hideko`. Most
commands support both systems, but some of them are limited in one way or another.
The bot currently supports SQLite as a database backend. A database file will be created after the first boot
in the same directory that you ran it. Do not delete the database file to avoid corruption and unpredictable
behavior.
+18 -2
View File
@@ -6,7 +6,7 @@
<groupId>wtf.beatrice.hidekobot</groupId> <groupId>wtf.beatrice.hidekobot</groupId>
<artifactId>HidekoBot</artifactId> <artifactId>HidekoBot</artifactId>
<version>0.5.0</version> <version>0.5.16</version>
<properties> <properties>
<maven.compiler.source>16</maven.compiler.source> <maven.compiler.source>16</maven.compiler.source>
@@ -18,7 +18,7 @@
<dependency> <dependency>
<groupId>net.dv8tion</groupId> <groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId> <artifactId>JDA</artifactId>
<version>5.0.0-alpha.22</version> <version>5.0.0-beta.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
@@ -40,6 +40,22 @@
<artifactId>snakeyaml</artifactId> <artifactId>snakeyaml</artifactId>
<version>1.33</version> <version>1.33</version>
</dependency> </dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220924</version>
</dependency>
</dependencies> </dependencies>
@@ -14,6 +14,10 @@ import wtf.beatrice.hidekobot.util.Logger;
import java.awt.*; import java.awt.*;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class Cache public class Cache
{ {
@@ -23,6 +27,16 @@ public class Cache
private static final String botPrefix = "hideko"; private static final String botPrefix = "hideko";
private static final Logger logger = new Logger(Cache.class); private static final Logger logger = new Logger(Cache.class);
// the Random instance that we should always use when looking for an RNG based thing.
// the seed is updated periodically.
private static final Random randomInstance = new Random();
// map to store results of "love calculator", to avoid people re-running the same command until
// they get what they wanted.
// i didn't think this was worthy of a whole database table with a runnable checking for expiration,
// and it will get cleared after a few minutes anyway, so RAM caching is more than good enough.
private static final HashMap<String, Integer> loveCalculatorValues = new HashMap<>();
private static PropertiesSource propertiesSource = null; private static PropertiesSource propertiesSource = null;
private static ConfigurationSource configurationSource = null; private static ConfigurationSource configurationSource = null;
private static DatabaseSource databaseSource = null; private static DatabaseSource databaseSource = null;
@@ -32,11 +46,17 @@ public class Cache
private final static String expiryTimestampFormat = "yy/MM/dd HH:mm:ss"; private final static String expiryTimestampFormat = "yy/MM/dd HH:mm:ss";
// note: discord sets interactions' expiry time to 15 minutes by default, so we can't go higher than that. // note: discord sets interactions' expiry time to 15 minutes by default, so we can't go higher than that.
private final static long expiryTimeSeconds = 15L; private final static long expiryTimeSeconds = 30L;
// used to count e.g. uptime // used to count e.g. uptime
private static LocalDateTime startupTime = null; private static LocalDateTime startupTime = null;
// date of when the first bot commit was made (CEST time)
private static final LocalDateTime botBirthDate = LocalDateTime.of(2022, 8, 25, 21, 50);
// the scheduler that should always be used when running a scheduled task.
private final static ScheduledExecutorService taskScheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources
private final static String execPath = System.getProperty("user.dir"); private final static String execPath = System.getProperty("user.dir");
private static final String botName = "Hideko"; private static final String botName = "Hideko";
@@ -60,6 +80,14 @@ public class Cache
*/ */
public static int[] getSupportedAvatarResolutions() { return supportedAvatarResolutions; } public static int[] getSupportedAvatarResolutions() { return supportedAvatarResolutions; }
public static Random getRandom() {
return randomInstance;
}
public static void setRandomSeed(long seed) {
randomInstance.setSeed(seed);
}
/** /**
* Checks if the bot has been started with the verbose argument. * Checks if the bot has been started with the verbose argument.
* *
@@ -214,7 +242,7 @@ public class Cache
try { try {
Field field = Color.class.getField(colorName); Field field = Color.class.getField(colorName);
color = (Color)field.get(null); color = (Color)field.get(null);
} catch (Exception e) { } catch (RuntimeException | NoSuchFieldException | IllegalAccessException e) {
logger.log("Unknown color: " + colorName); logger.log("Unknown color: " + colorName);
} }
return color == null ? defaultColor : color; return color == null ? defaultColor : color;
@@ -255,6 +283,13 @@ public class Cache
*/ */
public static LocalDateTime getStartupTime() { return startupTime; } public static LocalDateTime getStartupTime() { return startupTime; }
/**
* Get the time of when the bot was created.
*
* @return a LocalDateTime object of the first commit's instant.
*/
public static LocalDateTime getBotBirthDate() { return botBirthDate; }
public static String getFullHeartBeatLink() { public static String getFullHeartBeatLink() {
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.HEARTBEAT_LINK); return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.HEARTBEAT_LINK);
} }
@@ -274,4 +309,31 @@ public class Cache
*/ */
public static String getBotPrefix() { return botPrefix; } public static String getBotPrefix() { return botPrefix; }
public static void cacheLoveCalculatorValue(String userId1, String userId2, int value)
{
String merged = userId1 + "|" + userId2;
loveCalculatorValues.put(merged, value);
}
@Nullable
public static Integer getLoveCalculatorValue(String userId1, String userId2)
{
String merged1 = userId1 + "|" + userId2;
String merged2 = userId2 + "|" + userId1;
Integer value = null;
value = loveCalculatorValues.get(merged1);
if(value == null) value = loveCalculatorValues.get(merged2);
return value;
}
public static void removeLoveCalculatorValue(String userId1, String userId2)
{
loveCalculatorValues.remove(userId1 + "|" + userId2);
loveCalculatorValues.remove(userId2 + "|" + userId1);
}
public static ScheduledExecutorService getTaskScheduler() {
return taskScheduler;
}
} }
@@ -3,23 +3,21 @@ package wtf.beatrice.hidekobot;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.GatewayIntent;
import sun.misc.Signal; import sun.misc.Signal;
import wtf.beatrice.hidekobot.commands.completer.AvatarCommandCompleter; import wtf.beatrice.hidekobot.commands.completer.ProfileImageCommandCompleter;
import wtf.beatrice.hidekobot.commands.message.HelloCommand; import wtf.beatrice.hidekobot.commands.message.HelloCommand;
import wtf.beatrice.hidekobot.commands.slash.*; import wtf.beatrice.hidekobot.commands.slash.*;
import wtf.beatrice.hidekobot.datasources.ConfigurationSource; import wtf.beatrice.hidekobot.datasources.ConfigurationSource;
import wtf.beatrice.hidekobot.datasources.DatabaseSource; import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.datasources.PropertiesSource; import wtf.beatrice.hidekobot.datasources.PropertiesSource;
import wtf.beatrice.hidekobot.listeners.ButtonInteractionListener; import wtf.beatrice.hidekobot.listeners.*;
import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import wtf.beatrice.hidekobot.listeners.SlashCommandCompletionListener;
import wtf.beatrice.hidekobot.listeners.SlashCommandListener;
import wtf.beatrice.hidekobot.runnables.ExpiredMessageTask; import wtf.beatrice.hidekobot.runnables.ExpiredMessageTask;
import wtf.beatrice.hidekobot.runnables.HeartBeatTask; import wtf.beatrice.hidekobot.runnables.HeartBeatTask;
import wtf.beatrice.hidekobot.runnables.RandomSeedTask;
import wtf.beatrice.hidekobot.runnables.StatusUpdateTask;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.util.Logger; import wtf.beatrice.hidekobot.util.Logger;
import wtf.beatrice.hidekobot.util.SlashCommandUtil;
import java.io.File; import java.io.File;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -112,43 +110,68 @@ public class HidekoBot
SlashCommandListener slashCommandListener = new SlashCommandListener(); SlashCommandListener slashCommandListener = new SlashCommandListener();
SlashCommandCompletionListener slashCommandCompletionListener = new SlashCommandCompletionListener(); SlashCommandCompletionListener slashCommandCompletionListener = new SlashCommandCompletionListener();
AvatarCommand avatarCommand = new AvatarCommand(); AvatarCommand avatarCommand = new AvatarCommand();
AvatarCommandCompleter avatarCommandCompleter = new AvatarCommandCompleter(avatarCommand); ProfileImageCommandCompleter avatarCommandCompleter = new ProfileImageCommandCompleter(avatarCommand);
slashCommandListener.registerCommand(avatarCommand); slashCommandListener.registerCommand(avatarCommand);
slashCommandCompletionListener.registerCommandCompleter(avatarCommandCompleter); slashCommandCompletionListener.registerCommandCompleter(avatarCommandCompleter);
slashCommandListener.registerCommand(new BanCommand());
BannerCommand bannerCommand = new BannerCommand();
ProfileImageCommandCompleter bannerCommandCompleter = new ProfileImageCommandCompleter(bannerCommand);
slashCommandListener.registerCommand(bannerCommand);
slashCommandCompletionListener.registerCommandCompleter(bannerCommandCompleter);
slashCommandListener.registerCommand(new BotInfoCommand()); slashCommandListener.registerCommand(new BotInfoCommand());
slashCommandListener.registerCommand(new ClearCommand()); slashCommandListener.registerCommand(new ClearCommand());
slashCommandListener.registerCommand(new CoinFlipCommand()); slashCommandListener.registerCommand(new CoinFlipCommand());
slashCommandListener.registerCommand(new DiceRollCommand());
slashCommandListener.registerCommand(new DieCommand()); slashCommandListener.registerCommand(new DieCommand());
slashCommandListener.registerCommand(new HelpCommand()); slashCommandListener.registerCommand(new HelpCommand());
slashCommandListener.registerCommand(new InviteCommand()); slashCommandListener.registerCommand(new InviteCommand());
slashCommandListener.registerCommand(new KickCommand());
slashCommandListener.registerCommand(new LoveCalculatorCommand());
slashCommandListener.registerCommand(new MagicBallCommand());
slashCommandListener.registerCommand(new PingCommand()); slashCommandListener.registerCommand(new PingCommand());
slashCommandListener.registerCommand(new SayCommand()); slashCommandListener.registerCommand(new SayCommand());
Cache.setSlashCommandListener(slashCommandListener); slashCommandListener.registerCommand(new TimeoutCommand());
Cache.setSlashCommandCompletionListener(slashCommandCompletionListener); slashCommandListener.registerCommand(new TriviaCommand());
slashCommandListener.registerCommand(new UrbanDictionaryCommand());
// register message commands // register message commands
MessageCommandListener messageCommandListener = new MessageCommandListener(); MessageCommandListener messageCommandListener = new MessageCommandListener();
messageCommandListener.registerCommand(new HelloCommand()); messageCommandListener.registerCommand(new HelloCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.InviteCommand()); messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.AliasCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.AvatarCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BanCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BannerCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BotInfoCommand()); messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BotInfoCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.CoinFlipCommand()); messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.CoinFlipCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.ClearCommand()); messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.ClearCommand());
Cache.setMessageCommandListener(messageCommandListener); messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.DiceRollCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.HelpCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.InviteCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.KickCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.LoveCalculatorCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.MagicBallCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.SayCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.TimeoutCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.TriviaCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.UrbanDictionaryCommand());
// register listeners // register listeners
Cache.setSlashCommandListener(slashCommandListener);
Cache.setSlashCommandCompletionListener(slashCommandCompletionListener);
Cache.setMessageCommandListener(messageCommandListener);
jda.addEventListener(messageCommandListener); jda.addEventListener(messageCommandListener);
jda.addEventListener(slashCommandListener); jda.addEventListener(slashCommandListener);
jda.addEventListener(slashCommandCompletionListener); jda.addEventListener(slashCommandCompletionListener);
jda.addEventListener(new ButtonInteractionListener()); jda.addEventListener(new ButtonInteractionListener());
jda.addEventListener(new SelectMenuInteractionListener());
// update slash commands (delayed) // update slash commands (delayed)
final boolean finalForceUpdateCommands = forceUpdateCommands; final boolean finalForceUpdateCommands = forceUpdateCommands;
Executors.newSingleThreadScheduledExecutor().schedule(() -> // todo: try-with-resources Executors.newSingleThreadScheduledExecutor().schedule(() -> // todo: try-with-resources
SlashCommandUtil.updateSlashCommands(finalForceUpdateCommands), 1, TimeUnit.SECONDS); CommandUtil.updateSlashCommands(finalForceUpdateCommands), 1, TimeUnit.SECONDS);
// set the bot's status // set the bot's status
jda.getPresence().setStatus(OnlineStatus.ONLINE); jda.getPresence().setStatus(OnlineStatus.ONLINE);
jda.getPresence().setActivity(Activity.playing("Hatsune Miku: Project DIVA"));
// connect to database // connect to database
logger.log("Connecting to database..."); logger.log("Connecting to database...");
@@ -167,11 +190,15 @@ public class HidekoBot
} }
// start scheduled runnables // start scheduled runnables
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources ScheduledExecutorService scheduler = Cache.getTaskScheduler();
ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask(); ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask();
scheduler.scheduleAtFixedRate(expiredMessageTask, 5, 5, TimeUnit.SECONDS); //every 5 seconds scheduler.scheduleAtFixedRate(expiredMessageTask, 5, 5, TimeUnit.SECONDS); //every 5 seconds
HeartBeatTask heartBeatTask = new HeartBeatTask(); HeartBeatTask heartBeatTask = new HeartBeatTask();
scheduler.scheduleAtFixedRate(heartBeatTask, 10, 30, TimeUnit.SECONDS); //every 30 seconds scheduler.scheduleAtFixedRate(heartBeatTask, 10, 30, TimeUnit.SECONDS); //every 30 seconds
StatusUpdateTask statusUpdateTask = new StatusUpdateTask();
scheduler.scheduleAtFixedRate(statusUpdateTask, 0, 60 * 5, TimeUnit.SECONDS); // every 5 minutes
RandomSeedTask randomSeedTask = new RandomSeedTask();
scheduler.scheduleAtFixedRate(randomSeedTask, 0, 60, TimeUnit.SECONDS); // every minute
// register shutdown interrupt signal listener for proper shutdown. // register shutdown interrupt signal listener for proper shutdown.
Signal.handle(new Signal("INT"), signal -> shutdown()); Signal.handle(new Signal("INT"), signal -> shutdown());
@@ -0,0 +1,23 @@
package wtf.beatrice.hidekobot.commands.base;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
public class Alias
{
public static String generateNiceAliases(MessageCommand command)
{
LinkedList<String> aliases = command.getCommandLabels();
StringBuilder aliasesStringBuilder = new StringBuilder();
for(int i = 0; i < aliases.size(); i++)
{
aliasesStringBuilder.append("`").append(aliases.get(i)).append("`");
if(i + 1 != aliases.size())
aliasesStringBuilder.append(", "); // separate with comma except on last iteration
}
return aliasesStringBuilder.toString();
}
}
@@ -91,13 +91,18 @@ public class BotInfo
embedBuilder.addField("Maintainer", developerMention, true); embedBuilder.addField("Maintainer", developerMention, true);
// uptime field // uptime field
embedBuilder.addField("Uptime", FormatUtil.getNiceUptime(), true); embedBuilder.addField("Uptime", FormatUtil.getNiceTimeDiff(Cache.getStartupTime()), true);
// issue tracker field // issue tracker field
embedBuilder.addField("Support", embedBuilder.addField("Support",
"[Issue tracker](https://git.beatrice.wtf/mind-overflow/HidekoBot/issues)", "[Issue tracker](https://git.beatrice.wtf/mind-overflow/HidekoBot/issues)",
true); //todo: we should probably make this a final field in the config class true); //todo: we should probably make this a final field in the config class
// bot birthday field
embedBuilder.addField("Bot age",
Cache.getBotName() + " was created " + FormatUtil.getNiceTimeDiff(Cache.getBotBirthDate()) + "ago!",
false);
return embedBuilder.build(); return embedBuilder.build();
} }
} }
@@ -7,10 +7,7 @@ import net.dv8tion.jda.api.entities.channel.Channel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.buttons.Button;
import wtf.beatrice.hidekobot.Cache;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -46,12 +43,15 @@ public class ClearChat
return null; return null;
} }
public static int delete(int toDeleteAmount, long startingMessageId, MessageChannel channel) public static int delete(int toDeleteAmount,
long startingMessageId,
MessageChannel channel)
{ {
// int to keep track of how many messages we actually deleted. // int to keep track of how many messages we actually deleted.
int deleted = 0; int deleted = 0;
int limit = 95; //discord limits this method to range 2-100. we set it to 95 to be safe. int limit = 95; //discord limits this method to only 2<x<100 deletions per run.
// we set this slightly lower to be safe, and iterate as needed.
// increase the count by 1, because we technically aren't clearing the first ID ever // increase the count by 1, because we technically aren't clearing the first ID ever
// which is actually the slash command's ID and not a message. // which is actually the slash command's ID and not a message.
@@ -132,9 +132,8 @@ public class ClearChat
which are restricted by discord, and thus has to use which are restricted by discord, and thus has to use
a less efficient way that triggers rate-limiting very quickly. */ a less efficient way that triggers rate-limiting very quickly. */
} catch (Exception e) } catch (RuntimeException ignored)
{ {
return -1; return -1;
} }
} }
@@ -149,12 +148,13 @@ public class ClearChat
public static Button getDismissButton() public static Button getDismissButton()
{ {
return Button.primary("clear_dismiss", "Dismiss") return Button.primary("generic_dismiss", "Dismiss")
.withEmoji(Emoji.fromUnicode("")); .withEmoji(Emoji.fromUnicode(""));
} }
public static String parseAmount(int deleted) public static String parseAmount(int deleted)
{ {
if(deleted < 1) if(deleted < 1)
{ {
return "\uD83D\uDE22 Couldn't clear any message!"; return "\uD83D\uDE22 Couldn't clear any message!";
@@ -166,27 +166,7 @@ public class ClearChat
} }
} }
private void respond(Object responseFlowObj, String content) // cap the amount to avoid abuse.
{ public static int getMaxAmount() { return 1000; }
if(responseFlowObj instanceof InteractionHook) {
((InteractionHook) responseFlowObj).editOriginal(content).queue();
} else if (responseFlowObj instanceof Message) {
((Message) responseFlowObj).reply(content).queue();
}
}
public static void dismissMessage(ButtonInteractionEvent event)
{
if(!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
{
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
} else
{
event.getInteraction().getMessage().delete().queue();
}
}
} }
@@ -17,7 +17,7 @@ public class CoinFlip
public static Button getReflipButton() { public static Button getReflipButton() {
return Button.primary("coinflip_reflip", "Flip again") return Button.primary("coinflip_reflip", "Flip again")
.withEmoji(Emoji.fromFormatted("\uD83E\uDE99")); .withEmoji(Emoji.fromUnicode("\uD83E\uDE99"));
} }
public static String genRandom() public static String genRandom()
@@ -61,8 +61,6 @@ public class CoinFlip
public static void trackAndRestrict(Message replyMessage, User user) public static void trackAndRestrict(Message replyMessage, User user)
{ {
String replyMessageId = replyMessage.getId();
Cache.getDatabaseSource().queueDisabling(replyMessage); Cache.getDatabaseSource().queueDisabling(replyMessage);
Cache.getDatabaseSource().trackRanCommandReply(replyMessage, user); Cache.getDatabaseSource().trackRanCommandReply(replyMessage, user);
} }
@@ -0,0 +1,157 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.fun.Dice;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.UUID;
public class DiceRoll
{
public static MessageResponse buildResponse(User author, String[] args)
{
LinkedHashMap<Dice, Integer> dicesToRoll = new LinkedHashMap<>();
String diceRegex = "d[0-9]+";
String amountRegex = "[0-9]+";
Dice currentDice = null;
int currentAmount;
UUID lastPushedDice = null;
int totalRolls = 0;
for(String arg : args)
{
if(totalRolls > 200)
{
return new MessageResponse("Too many total rolls!", null);
}
if(arg.matches(amountRegex))
{
currentAmount = Integer.parseInt(arg);
if(currentDice == null)
{
currentDice = new Dice(6);
} else {
currentDice = new Dice(currentDice);
}
if(currentAmount > 100)
{
return new MessageResponse("Too many rolls (`" + currentAmount + "`)!", null);
}
lastPushedDice = currentDice.getUUID();
dicesToRoll.put(currentDice, currentAmount);
totalRolls += currentAmount;
}
else if(arg.matches(diceRegex))
{
int sides = Integer.parseInt(arg.substring(1));
if(sides > 10000)
{
return new MessageResponse("Too many sides (`" + sides + "`)!", null);
}
if(args.length == 1)
{
dicesToRoll.put(new Dice(sides), 1);
totalRolls++;
} else
{
if(currentDice != null)
{
if(lastPushedDice == null || !lastPushedDice.equals(currentDice.getUUID()))
{
dicesToRoll.put(currentDice, 1);
lastPushedDice = currentDice.getUUID();
totalRolls++;
}
}
currentDice = new Dice(sides);
}
}
}
if(lastPushedDice == null)
{
if(currentDice != null)
{
dicesToRoll.put(currentDice, 1);
totalRolls++;
}
} else
{
if(!lastPushedDice.equals(currentDice.getUUID()))
{
dicesToRoll.put(new Dice(currentDice), 1);
totalRolls++;
}
}
LinkedList<Dice> rolledDices = new LinkedList<>();
// in case no dice was specified (or invalid), roll a standard 6-sided dice.
if(dicesToRoll.isEmpty())
{
Dice standardDice = new Dice(6);
dicesToRoll.put(standardDice, 1);
totalRolls = 1;
}
for(Dice dice : dicesToRoll.keySet())
{
for(int roll = 0; roll < dicesToRoll.get(dice); roll++)
{
dice.roll();
rolledDices.add(new Dice(dice));
}
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setTitle("Dice Roll");
StringBuilder message = new StringBuilder();
int total = 0;
int previousDiceSides = 0;
for (Dice dice : rolledDices) {
int diceSize = dice.getSides();
if (previousDiceSides != diceSize) {
message.append("\nd").append(diceSize).append(": ");
previousDiceSides = diceSize;
} else if (previousDiceSides != 0) {
message.append(", ");
}
message.append("`").append(dice.getValue()).append("`");
total += dice.getValue();
}
// discord doesn't allow embed fields to be longer than 1024 and errors out
if(message.length() > 1024)
{
return new MessageResponse("Too many rolls!", null);
}
embedBuilder.addField("\uD83C\uDFB2 Rolls", message.toString(), false);
String rolls = totalRolls == 1 ? "roll" : "rolls";
embedBuilder.addField("✨ Total", totalRolls + " " + rolls + ": " + total, false);
return new MessageResponse(null, embedBuilder.build());
}
}
@@ -0,0 +1,48 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.concurrent.TimeUnit;
public class LoveCalculator
{
public static MessageEmbed buildEmbedAndCacheResult(User author, User user1, User user2)
{
String userId1 = user1.getId();
String userId2 = user2.getId();
Integer loveAmount = Cache.getLoveCalculatorValue(userId1, userId2);
if(loveAmount == null)
{
loveAmount = RandomUtil.getRandomNumber(0, 100);
Cache.cacheLoveCalculatorValue(userId1, userId2, loveAmount);
Cache.getTaskScheduler().schedule(() ->
Cache.removeLoveCalculatorValue(userId1, userId2), 10, TimeUnit.MINUTES);
}
String formattedAmount = loveAmount + "%";
if(loveAmount <= 30) formattedAmount += "... \uD83D\uDE22";
else if(loveAmount < 60) formattedAmount += "! \uD83E\uDDD0";
else if(loveAmount < 75) formattedAmount += "!!! \uD83E\uDD73";
else formattedAmount = "" + formattedAmount + "!!! \uD83D\uDE0D\uD83D\uDCA5";
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setTitle("Love Calculator");
embedBuilder.addField("\uD83D\uDC65 People",
user1.getAsMention() + " & " + user2.getAsMention(),
false);
embedBuilder.addField("❤️\u200D\uD83D\uDD25 Match",
formattedAmount,
false);
return embedBuilder.build();
}
}
@@ -0,0 +1,67 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class MagicBall
{
public static LinkedList<String> getLabels()
{
return new LinkedList<>(Arrays.asList("8ball", "8b", "eightball", "magicball"));
}
private final static List<String> answers = new ArrayList<>(
Arrays.asList("It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes, definitely.",
"That would be a yes.",
"As I see it, yes.",
"Most likely.",
"Looks like it.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Seems uncertain.",
"Concentrate and ask again.",
"Don't count on it.",
"My answer is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful."));
public static String getRandomAnswer()
{
int answerPos = RandomUtil.getRandomNumber(0, answers.size() - 1);
return answers.get(answerPos);
}
public static MessageEmbed generateEmbed(String question, User author)
{
// add a question mark at the end, if missing.
// this might not always apply but it's fun
if(!question.endsWith("?")) question += "?";
String answer = getRandomAnswer();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setTitle("Magic Ball");
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.addField("❓ Question", question, false);
embedBuilder.addField("\uD83C\uDFB1 Answer", answer, false);
return embedBuilder.build();
}
}
@@ -0,0 +1,102 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.utils.ImageProxy;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
public class ProfileImage
{
public static int parseResolution(int resolution)
{
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
// method to find closest value to accepted values
int distance = Math.abs(acceptedSizes[0] - resolution);
int idx = 0;
for(int c = 1; c < acceptedSizes.length; c++){
int cdistance = Math.abs(acceptedSizes[c] - resolution);
if(cdistance < distance){
idx = c;
distance = cdistance;
}
}
return acceptedSizes[idx];
}
public static MessageResponse buildResponse(int resolution, User user, ImageType imageType)
{
String imageTypeName = imageType.name().toLowerCase();
String resolutionString;
String imageLink = null;
User.Profile userProfile = user.retrieveProfile().complete();
ImageProxy bannerProxy = userProfile.getBanner();
if(imageType == ImageType.AVATAR)
{
resolutionString = resolution + " × " + resolution;
imageLink = user.getEffectiveAvatar().getUrl(resolution);
} else {
int verticalRes = 361 * resolution / 1024;
resolutionString = resolution + " × " + verticalRes;
if(bannerProxy != null)
imageLink = bannerProxy.getUrl(resolution);
}
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("Profile " + imageTypeName);
embedBuilder.addField("User", user.getAsMention(), false);
embedBuilder.addField("Current resolution", resolutionString, false);
// string builder to create a string that links to all available resolutions
StringBuilder links = new StringBuilder();
for(int pos = 0; pos < acceptedSizes.length; pos++)
{
int currSize = acceptedSizes[pos];
String currLink;
if(imageType == ImageType.AVATAR)
{
currLink = user.getEffectiveAvatar().getUrl(currSize);
} else {
if(bannerProxy == null) break;
currLink = bannerProxy.getUrl(currSize);
}
links.append("**[").append(currSize).append("px](").append(currLink).append(")**");
if(pos + 1 != acceptedSizes.length) // don't add a separator on the last iteration
{
links.append(" | ");
}
}
embedBuilder.addField("Available resolutions", links.toString(), false);
if(imageLink != null)
embedBuilder.setImage(imageLink);
if(imageLink == null) {
String error = "I couldn't find " + user.getAsMention() + "'s " + imageTypeName + "!";
return new MessageResponse(error, null);
} else {
return new MessageResponse(null, embedBuilder.build());
}
}
public enum ImageType
{
AVATAR, BANNER;
}
}
@@ -0,0 +1,11 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.Permission;
public class Say
{
public static Permission getPermission() {
return Permission.MESSAGE_MANAGE;
}
}
@@ -0,0 +1,284 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import org.apache.commons.text.StringEscapeUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.comparators.TriviaCategoryComparator;
import wtf.beatrice.hidekobot.objects.fun.TriviaCategory;
import wtf.beatrice.hidekobot.objects.fun.TriviaQuestion;
import wtf.beatrice.hidekobot.objects.fun.TriviaScore;
import wtf.beatrice.hidekobot.runnables.TriviaTask;
import wtf.beatrice.hidekobot.util.CommandUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class Trivia
{
private final static String triviaLink = "https://opentdb.com/api.php?amount=10&type=multiple&category=";
private final static String categoriesLink = "https://opentdb.com/api_category.php";
public static List<String> channelsRunningTrivia = new ArrayList<>();
// first string is the channelId, the list contain all users who responded there
public static HashMap<String, List<String>> channelAndWhoResponded = new HashMap<>();
// first string is the channelId, the list contain all score records for that channel
public static HashMap<String, LinkedList<TriviaScore>> channelAndScores = new HashMap<>();
public static String getTriviaLink(int categoryId) {return triviaLink + categoryId; }
public static String getCategoriesLink() {return categoriesLink; }
public static String getNoDMsError() {
return "\uD83D\uDE22 Sorry! Trivia doesn't work in DMs.";
}
public static String getTriviaAlreadyRunningError() {
// todo nicer looking
return "Trivia is already running here!";
}
public static MessageResponse generateMainScreen()
{
// todo null checks
JSONObject categoriesJson = Trivia.fetchJson(Trivia.getCategoriesLink());
List<TriviaCategory> categories = Trivia.parseCategories(categoriesJson);
categories.sort(new TriviaCategoryComparator());
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("\uD83C\uDFB2 Trivia");
embedBuilder.addField("\uD83D\uDCD6 Begin here",
"Select a category from the dropdown menu to start a match!",
false);
embedBuilder.addField("❓ How to play",
"A new question gets posted every few seconds." +
"\nIf you get it right, you earn points!" +
"\nIf you choose a wrong answer, you lose points." +
"\nIf you are unsure, you can wait without answering and your score won't change!",
false);
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("trivia_categories");
for(TriviaCategory category : categories)
{
String name = category.categoryName();
int id = category.categoryId();
menuBuilder.addOption(name, String.valueOf(id));
}
return new MessageResponse(null, embedBuilder.build(), menuBuilder.build());
}
public static JSONObject fetchJson(String link)
{
try {
URL url = new URL(link);
URLConnection connection = url.openConnection();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String currentChar;
StringBuilder jsonStrBuilder = new StringBuilder();
while((currentChar = bufferedReader.readLine()) != null)
{
jsonStrBuilder.append(currentChar);
}
bufferedReader.close();
return new JSONObject(jsonStrBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static List<TriviaQuestion> parseQuestions(JSONObject jsonObject)
{
List<TriviaQuestion> questions = new ArrayList<>();
JSONArray results = jsonObject.getJSONArray("results");
for(Object currentQuestionGeneric : results)
{
JSONObject questionJson = (JSONObject) currentQuestionGeneric;
String question = StringEscapeUtils.unescapeHtml4(questionJson.getString("question"));
String correctAnswer = StringEscapeUtils.unescapeHtml4(questionJson.getString("correct_answer"));
List<String> incorrectAnswersList = new ArrayList<>();
JSONArray incorrectAnswers = questionJson.getJSONArray("incorrect_answers");
for(Object incorrectAnswerGeneric : incorrectAnswers)
{
String incorrectAnswer = (String) incorrectAnswerGeneric;
incorrectAnswersList.add(StringEscapeUtils.unescapeHtml4(incorrectAnswer));
}
TriviaQuestion triviaQuestion = new TriviaQuestion(question, correctAnswer, incorrectAnswersList);
questions.add(triviaQuestion);
}
return questions;
}
public static List<TriviaCategory> parseCategories(JSONObject jsonObject)
{
List<TriviaCategory> categories = new ArrayList<>();
JSONArray categoriesArray = jsonObject.getJSONArray("trivia_categories");
for(Object categoryObject : categoriesArray)
{
JSONObject categoryJson = (JSONObject) categoryObject;
String name = categoryJson.getString("name");
int id = categoryJson.getInt("id");
categories.add(new TriviaCategory(name, id));
}
return categories;
}
public static void handleAnswer(ButtonInteractionEvent event, AnswerType answerType)
{
User user = event.getUser();
String channelId = event.getChannel().getId();
if(trackResponse(user, event.getChannel()))
{
LinkedList<TriviaScore> scores = channelAndScores.get(channelId);
if(scores == null) scores = new LinkedList<>();
TriviaScore currentUserScore = null;
for(TriviaScore score : scores)
{
if(score.getUser().equals(user))
{
currentUserScore = score;
scores.remove(score);
break;
}
}
if(currentUserScore == null)
{
currentUserScore = new TriviaScore(user);
}
if(answerType.equals(AnswerType.CORRECT))
{
event.reply(user.getAsMention() + " got it right! \uD83E\uDD73 (**+3**)").queue();
currentUserScore.changeScore(3);
} else {
event.reply("" + user.getAsMention() + ", that's not the right answer! (**-1**)").queue();
currentUserScore.changeScore(-1);
}
scores.add(currentUserScore);
channelAndScores.put(channelId, scores);
} else {
event.reply("☹️ " + user.getAsMention() + ", you can't answer twice!")
.queue(interaction ->
Cache.getTaskScheduler().schedule(() ->
interaction.deleteOriginal().queue(), 3, TimeUnit.SECONDS));
}
}
private static boolean trackResponse(User user, MessageChannel channel)
{
String userId = user.getId();
String channelId = channel.getId();
List<String> responders = channelAndWhoResponded.get(channelId);
if(responders == null)
{
responders = new ArrayList<>();
}
if(responders.isEmpty() || !responders.contains(userId))
{
responders.add(userId);
channelAndWhoResponded.put(channelId, responders);
return true; // response was successfully tracked
} else {
return false; // response wasn't tracked because there already was an entry
}
}
public static void handleMenuSelection(StringSelectInteractionEvent event)
{
// check if the user interacting is the same one who ran the command
if(!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
{
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// todo: we shouldn't use this method, since it messes with the database... look at coin reflip
CommandUtil.disableExpired(event.getMessageId());
SelectOption pickedOption = event.getInteraction().getSelectedOptions().get(0);
String categoryName = pickedOption.getLabel();
String categoryIdString = pickedOption.getValue();
Integer categoryId = Integer.parseInt(categoryIdString);
TriviaCategory category = new TriviaCategory(categoryName, categoryId);
startTrivia(event, category);
}
public static void startTrivia(StringSelectInteractionEvent event, TriviaCategory category)
{
User author = event.getUser();
Message message = event.getMessage();
MessageChannel channel = message.getChannel();
if(Trivia.channelsRunningTrivia.contains(channel.getId()))
{
// todo nicer looking
// todo: also what if the bot stops (database...?)
// todo: also what if the message is already deleted
Message err = event.reply("Trivia is already running here!").complete().retrieveOriginal().complete();
Cache.getTaskScheduler().schedule(() -> err.delete().queue(), 10, TimeUnit.SECONDS);
return;
} else {
// todo nicer looking
event.reply("Starting new Trivia session!").queue();
}
TriviaTask triviaTask = new TriviaTask(author, channel, category);
ScheduledFuture<?> future =
Cache.getTaskScheduler().scheduleAtFixedRate(triviaTask,
0,
15,
TimeUnit.SECONDS);
triviaTask.setScheduledFuture(future);
Trivia.channelsRunningTrivia.add(channel.getId());
}
public enum AnswerType {
CORRECT, WRONG
}
}
@@ -0,0 +1,323 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.text.WordUtils;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.util.SerializationUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class UrbanDictionary
{
public static LinkedList<String> getCommandLabels()
{ return new LinkedList<>(Arrays.asList("urban", "urbandictionary", "ud")); }
public static String getBaseUrl() {
return "https://www.urbandictionary.com/define.php?term=";
}
public static Button getPreviousPageButton()
{
return Button.primary("urban_previouspage", "Back")
.withEmoji(Emoji.fromFormatted("⬅️"));
}
public static Button getNextPageButton()
{
return Button.primary("urban_nextpage", "Next")
.withEmoji(Emoji.fromFormatted("➡️"));
}
public static Button getDeleteButton()
{
return Button.danger("generic_dismiss", "Delete")
.withEmoji(Emoji.fromFormatted("\uD83D\uDDD1"));
}
public static String getNoArgsError() {
return "\uD83D\uDE22 I need to know what to search for!";
}
public static String sanitizeArgs(String term, boolean forUrl)
{
term = term.replaceAll("[^\\w\\s]", ""); // only keep letters, numbers and spaces
term = WordUtils.capitalizeFully(term); // Make Every Word Start With A Capital Letter
if(forUrl) term = term.replaceAll("\\s+", "+"); // replace all whitespaces with + for the url
if (term.length() > 64) term = term.substring(0, 64); // cut it to length to avoid abuse
return term;
}
public static String generateUrl(String term)
{
return getBaseUrl() + sanitizeArgs(term, true);
}
public static MessageEmbed buildEmbed(String term,
String url,
User author,
UrbanSearch search,
int page)
{
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle(term + ", on Urban Dictionary", url);
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.addField("\uD83D\uDCD6 Definition", search.getPlaintextMeanings().get(page), false);
embedBuilder.addField("\uD83D\uDCAD Example", search.getPlaintextExamples().get(page), false);
embedBuilder.addField("\uD83D\uDCCC Submission",
"*Entry " + (page+1) + " | Sent by " + search.getContributorsNames().get(page) +
" on" + search.getSubmissionDates().get(page) + "*",
false);
return embedBuilder.build();
}
public static String getTermNotFoundError()
{
return "\uD83D\uDE22 I couldn't find that term!";
}
public static void track(Message message, User user, UrbanSearch search, String sanitizedTerm)
{
Cache.getDatabaseSource().queueDisabling(message);
Cache.getDatabaseSource().trackRanCommandReply(message, user);
Cache.getDatabaseSource().trackUrban(search.getSerializedMeanings(),
search.getSerializedExamples(),
search.getSerializedContributors(),
search.getSerializedDates(),
message,
sanitizedTerm);
}
public static void changePage(ButtonInteractionEvent event, ChangeType changeType)
{
String messageId = event.getMessageId();
DatabaseSource database = Cache.getDatabaseSource();
// check if the user interacting is the same one who ran the command
if (!(database.isUserTrackedFor(event.getUser().getId(), messageId))) {
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// get current page and calculate how many pages there are
int page = Cache.getDatabaseSource().getUrbanPage(messageId);
String term = database.getUrbanTerm(messageId);
String url = generateUrl(term);
// get serialized parameters
String serializedMeanings = database.getUrbanMeanings(messageId);
String serializedExamples = database.getUrbanExamples(messageId);
String serializedContributors = database.getUrbanContributors(messageId);
String serializedDates = database.getUrbanDates(messageId);
// construct object by passing serialized parameters
UrbanSearch search = new UrbanSearch(serializedMeanings,
serializedExamples, serializedContributors, serializedDates);
// move to new page
if(changeType == ChangeType.NEXT)
page++;
else if(changeType == ChangeType.PREVIOUS)
page--;
term = UrbanDictionary.sanitizeArgs(term, false);
// generate embed with new results
MessageEmbed updatedEmbed = UrbanDictionary.buildEmbed(term, url, event.getUser(), search, page);
// get all attached components and check which ones need to be enabled or disabled
List<ItemComponent> components = new ArrayList<>();
if(page > 0)
{
components.add(UrbanDictionary.getPreviousPageButton().asEnabled());
} else {
components.add(UrbanDictionary.getPreviousPageButton().asDisabled());
}
if(page + 1 == search.getPages())
{
components.add(UrbanDictionary.getNextPageButton().asDisabled());
} else {
components.add(UrbanDictionary.getNextPageButton().asEnabled());
}
// update the components on the object
components.add(UrbanDictionary.getDeleteButton());
ActionRow currentRow = ActionRow.of(components);
// update the message
event.editComponents(currentRow).setEmbeds(updatedEmbed).queue();
database.setUrbanPage(messageId, page);
database.resetExpiryTimestamp(messageId);
}
public static class UrbanSearch
{
final LinkedList<String> plaintextMeanings;
final LinkedList<String> plaintextExamples;
final LinkedList<String> contributorsNames;
final LinkedList<String> submissionDates;
final String serializedMeanings;
final String serializedExamples;
final String serializedContributors;
final String serializedDates;
final int pages;
public UrbanSearch(String serializedMeanings,
String serializedExamples,
String serializedContributors,
String serializedDates)
{
this.serializedMeanings = serializedMeanings;
this.serializedExamples = serializedExamples;
this.serializedContributors = serializedContributors;
this.serializedDates = serializedDates;
this.plaintextMeanings = SerializationUtil.deserializeBase64(serializedMeanings);
this.plaintextExamples = SerializationUtil.deserializeBase64(serializedExamples);
this.contributorsNames = SerializationUtil.deserializeBase64(serializedContributors);
this.submissionDates = SerializationUtil.deserializeBase64(serializedDates);
this.pages = submissionDates.size();
}
public UrbanSearch(Elements definitions)
{
plaintextMeanings = new LinkedList<>();
plaintextExamples = new LinkedList<>();
contributorsNames = new LinkedList<>();
submissionDates = new LinkedList<>();
for(Element definition : definitions)
{
Elements meaningSingleton = definition.getElementsByClass("meaning");
if(meaningSingleton.isEmpty())
{
plaintextMeanings.add(" ");
} else
{
Element meaning = meaningSingleton.get(0);
String text = meaning.html()
.replaceAll("<br\\s*?>", "\n") // keep newlines
.replaceAll("<.*?>", ""); // remove all other html tags
// this is used to fix eg. &amp; being shown literally instead of being parsed
text = StringEscapeUtils.unescapeHtml4(text);
// discord only allows 1024 characters for embed fields
if(text.length() > 1024) text = text.substring(0, 1020) + "...";
plaintextMeanings.add(text);
}
Elements exampleSingleton = definition.getElementsByClass("example");
if(exampleSingleton.isEmpty())
{
plaintextExamples.add(" ");
} else
{
Element example = exampleSingleton.get(0);
String text = example.html()
.replaceAll("<br\\s*?>", "\n") // keep newlines
.replaceAll("<.*?>", ""); // remove all other html tags
// this is used to fix eg. &amp; being shown literally instead of being parsed
text = StringEscapeUtils.unescapeHtml4(text);
// discord only allows 1024 characters for embed fields
if(text.length() > 1024) text = text.substring(0, 1020) + "...";
plaintextExamples.add(text);
}
Elements contributorSingleton = definition.getElementsByClass("contributor");
if(contributorSingleton.isEmpty())
{
contributorsNames.add("Unknown");
} else
{
Element contributor = contributorSingleton.get(0);
String htmlContributor = contributor.html();
String htmlContributorName = contributor.select("a").html();
String htmlSubmitDate = htmlContributor.substring(
htmlContributor.indexOf("</a>") + 4);
contributorsNames.add(htmlContributorName
.replaceAll("<.*?>", "")); // remove all html tags;
submissionDates.add(htmlSubmitDate
.replaceAll("<.*?>", "")); // remove all html tags;
}
}
serializedMeanings = SerializationUtil.serializeBase64(plaintextMeanings);
serializedExamples = SerializationUtil.serializeBase64(plaintextExamples);
serializedContributors = SerializationUtil.serializeBase64(contributorsNames);
serializedDates = SerializationUtil.serializeBase64(submissionDates);
pages = submissionDates.size();
}
public List<String> getPlaintextMeanings() {
return this.plaintextMeanings;
}
public List<String> getPlaintextExamples() {
return this.plaintextExamples;
}
public List<String> getContributorsNames() {
return this.contributorsNames;
}
public List<String> getSubmissionDates() {
return this.submissionDates;
}
public String getSerializedMeanings() {
return serializedMeanings;
}
public String getSerializedExamples() {
return serializedExamples;
}
public String getSerializedContributors() {
return serializedContributors;
}
public String getSerializedDates() {
return serializedDates;
}
public int getPages() {
return pages;
}
}
public enum ChangeType
{
NEXT,
PREVIOUS;
}
}
@@ -0,0 +1,259 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import org.apache.commons.lang3.ArrayUtils;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.util.FormatUtil;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class UserPunishment
{
private final static Duration maxTimeoutDuration = Duration.of(28, ChronoUnit.DAYS);
private final static Duration minTimeoutDuration = Duration.of(30, ChronoUnit.SECONDS);
public static void handle(SlashCommandInteractionEvent event, PunishmentType punishmentType)
{
// this might take a sec
event.deferReply().queue();
User targetUser = null;
OptionMapping targetUserArg = event.getOption("target");
if(targetUserArg != null)
{
targetUser = targetUserArg.getAsUser();
}
List<IMentionable> mentions = null;
if(targetUser != null) mentions = new ArrayList<>(Collections.singletonList(targetUser));
String reason = null;
OptionMapping reasonArg = event.getOption("reason");
if(reasonArg != null)
{
reason = reasonArg.getAsString();
}
String timeDiff = null;
OptionMapping timeDiffArg = event.getOption("duration");
if(timeDiffArg != null)
{
timeDiff = timeDiffArg.getAsString();
}
// todo: the following code is not great, because we are making an array and then
// we are also recreating the string later in code. this is useless and a bit hacked on,
// but works for now. this happened because the function was NOT written with slash commands
// in mind, but with message commands, that send every word as a separate argument.
// we should probably rework the it so that it works better in both scenarios.
String[] reasonSplit = null;
// generate the arguments array by splitting the string
if(reason != null) reasonSplit = reason.split("\\s+");
//prepend timediff at index 0
if(timeDiff != null) reasonSplit = ArrayUtils.insert(0, reasonSplit, timeDiff);
// in message-commands, the first arg would contain the user mention. since we have no one mentioned here,
// because it's in its own argument, we just prepend an empty string. note that this makes relying on the
// first argument BAD, because it is no longer ensured that it contains the user mention.
if(timeDiff != null) reasonSplit = ArrayUtils.insert(0, reasonSplit, "");
MessageResponse response = getResponse(event.getUser(),
punishmentType,
event.getChannel(),
mentions,
reasonSplit);
if(response.embed() != null)
event.getHook().editOriginalEmbeds(response.embed()).queue();
else if(response.content() != null)
event.getHook().editOriginal(response.content()).queue();
}
public static void handle(MessageReceivedEvent event, String[] args, PunishmentType punishmentType)
{
Mentions msgMentions = event.getMessage().getMentions();
List<IMentionable> mentions = msgMentions.getMentions();
MessageResponse response = getResponse(event.getAuthor(),
punishmentType,
event.getChannel(),
mentions,
args);
if(response.embed() != null)
event.getMessage().replyEmbeds(response.embed()).queue();
else if(response.content() != null)
event.getMessage().reply(response.content()).queue();
}
public static MessageResponse getResponse(User author,
PunishmentType punishmentType,
MessageChannelUnion channel,
List<IMentionable> mentions,
String[] args)
{
String punishmentTypeName = punishmentType.name().toLowerCase();
if(!(channel instanceof TextChannel))
{
// todo nicer looking with emojis
return new MessageResponse("Sorry! I can't " + punishmentTypeName + " people in DMs.", null);
}
if(mentions == null || mentions.isEmpty())
{
// todo nicer looking with emojis
return new MessageResponse("You have to tell me who to " + punishmentTypeName + "!", null);
}
String mentionedId = mentions.get(0).getId();
User mentioned = null;
try {
mentioned = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
} catch (RuntimeException ignored)
{
// todo nicer looking with emojis
return new MessageResponse("I can't " + punishmentTypeName + " that user!", null);
}
StringBuilder reasonBuilder = new StringBuilder();
String reason = "";
// some commands require an additional parameter before the reason, so in that case, we should start at 2.
int startingPoint = punishmentType == PunishmentType.TIMEOUT ? 2 : 1;
if(args != null && args.length > startingPoint)
{
for(int i = startingPoint; i < args.length; i++)
{
String arg = args[i];
reasonBuilder.append(arg);
if(i + 1 != arg.length())
reasonBuilder.append(" "); // separate args with a space except on last iteration.
}
reason = reasonBuilder.toString();
}
if(mentioned == null)
{
// todo nicer looking with emojis
return new MessageResponse("I can't " + punishmentTypeName + " that user!", null);
}
Guild guild = ((TextChannel) channel).getGuild();
Duration duration = null;
AuditableRestAction<Void> punishmentAction = null;
try {
switch (punishmentType) {
case BAN -> punishmentAction = guild.ban(mentioned, 0, TimeUnit.SECONDS);
case KICK -> punishmentAction = guild.kick(mentioned);
case TIMEOUT -> {
if(args != null)
{
String durationStr = args[1];
duration = FormatUtil.parseDuration(durationStr);
}
boolean isDurationValid = true;
if(duration == null) isDurationValid = false;
else
{
if(duration.compareTo(maxTimeoutDuration) > 0) isDurationValid = false;
if(minTimeoutDuration.compareTo(duration) > 0) isDurationValid = false;
}
if(duration == null || !isDurationValid)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, but the specified duration is invalid!", null);
}
punishmentAction = guild.timeoutFor(mentioned, duration);
}
}
} catch (RuntimeException ignored) {
// todo nicer looking with emojis
return new MessageResponse("Sorry, I couldn't " + punishmentTypeName + " " + mentioned.getAsMention() + "!",
null);
}
if(!reason.isEmpty() && !reasonBuilder.isEmpty())
punishmentAction.reason("[" + author.getAsTag() + "] " + reason);
try {
punishmentAction.complete();
} catch (RuntimeException ignored)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, I couldn't " + punishmentTypeName + " " + mentioned.getAsMention() + "!",
null);
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("User " + punishmentType.getPastTense());
embedBuilder.addField("\uD83D\uDC64 User", mentioned.getAsMention(), false);
embedBuilder.addField("✂️ By", author.getAsMention(), false);
if(duration != null)
embedBuilder.addField("⏱️ Duration", FormatUtil.getNiceDuration(duration), false);
if(reason.isEmpty())
reason = "*No reason specified*";
embedBuilder.addField("\uD83D\uDCD6 Reason", reason, false);
return new MessageResponse(null, embedBuilder.build());
}
public enum PunishmentType {
KICK("kicked"),
BAN("banned"),
TIMEOUT("timed out"),
;
private final String pastTense;
PunishmentType(String pastTense)
{
this.pastTense = pastTense;
}
public String getPastTense()
{
return pastTense;
}
}
}
@@ -10,10 +10,10 @@ import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class AvatarCommandCompleter extends SlashArgumentsCompleterImpl public class ProfileImageCommandCompleter extends SlashArgumentsCompleterImpl
{ {
public AvatarCommandCompleter(SlashCommand parentCommand) { public ProfileImageCommandCompleter(SlashCommand parentCommand) {
super(parentCommand); super(parentCommand);
} }
@@ -0,0 +1,78 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Alias;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class AliasCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Arrays.asList("alias", "aliases"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.TOOLS;
}
@NotNull
@Override
public String getDescription() {
return "See other command aliases.";
}
@Nullable
@Override
public String getUsage() {
return "<command>";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
if(args.length == 0)
{
event.getMessage().reply("\uD83D\uDE20 Hey, you have to specify a command!").queue();
return;
}
String commandLabel = args[0].toLowerCase();
MessageCommand command = Cache.getMessageCommandListener().getRegisteredCommand(commandLabel);
if(command == null)
{
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
return;
}
String aliases = Alias.generateNiceAliases(command);
aliases = "Aliases for **" + command.getCommandLabels().get(0) + "**: " + aliases;
event.getMessage()
.reply(aliases)
.queue();
}
}
@@ -0,0 +1,106 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class AvatarCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("avatar"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Get someone's avatar, or your own. You can additionally specify a resolution.";
}
@Nullable
@Override
public String getUsage() {
return "[mentioned user] [resolution]";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
User user;
int resolution = -1;
// we have no specific order for user and resolution, so let's try parsing any arg as resolution
// (mentions are handled differently by a specific method)
boolean resFound = false;
for (String arg : args) {
try {
int givenRes = Integer.parseInt(arg);
resolution = ProfileImage.parseResolution(givenRes);
resFound = true;
break;
} catch (NumberFormatException ignored) {
}
}
// fallback in case we didn't find any specified resolution
if(!resFound) resolution = ProfileImage.parseResolution(512);
// check if someone is mentioned
Mentions mentions = event.getMessage().getMentions();
if(mentions.getMentions().isEmpty())
{
user = event.getAuthor();
} else
{
String mentionedId = mentions.getMentions().get(0).getId();
user = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
}
// in case of issues, fallback to the sender
if(user == null) user = event.getAuthor();
// send a response
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if(response.content() != null)
{
event.getMessage().reply(response.content()).queue();
} else if(response.embed() != null)
{
event.getMessage().replyEmbeds(response.embed()).queue();
}
}
}
@@ -0,0 +1,68 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class BanCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("ban"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return new ArrayList<Permission>(Collections.singletonList(Permission.BAN_MEMBERS));
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
return "Ban the mentioned user.";
}
@Nullable
@Override
public String getUsage() {
return "<mentioned user> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.BAN);
}
}
@@ -0,0 +1,103 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BannerCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("banner"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Get someone's profile banner, or your own.";
}
@Nullable
@Override
public String getUsage() {
return "[mentioned user] [resolution]";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
User user;
int resolution = -1;
// we have no specific order for user and resolution, so let's try parsing any arg as resolution
// (mentions are handled differently by a specific method)
boolean resFound = false;
for (String arg : args) {
try {
int givenRes = Integer.parseInt(arg);
resolution = ProfileImage.parseResolution(givenRes);
resFound = true;
break;
} catch (NumberFormatException ignored) {
}
}
// fallback in case we didn't find any specified resolution
if(!resFound) resolution = ProfileImage.parseResolution(512);
// check if someone is mentioned
Mentions mentions = event.getMessage().getMentions();
if(mentions.getMentions().isEmpty())
{
user = event.getAuthor();
} else
{
String mentionedId = mentions.getMentions().get(0).getId();
user = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
}
// in case of issues, fallback to the sender
if(user == null) user = event.getAuthor();
// send a response
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
if(response.content() != null)
{
event.getMessage().reply(response.content()).queue();
} else if(response.embed() != null)
{
event.getMessage().replyEmbeds(response.embed()).queue();
}
}
}
@@ -3,11 +3,14 @@ package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache; import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo; import wtf.beatrice.hidekobot.commands.base.BotInfo;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand; import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -17,7 +20,7 @@ public class BotInfoCommand implements MessageCommand
@Override @Override
public LinkedList<String> getCommandLabels() { public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("botinfo")); return new LinkedList<>(Arrays.asList("botinfo", "info"));
} }
@Nullable @Nullable
@@ -31,6 +34,24 @@ public class BotInfoCommand implements MessageCommand
return false; return false;
} }
@NotNull
@Override
public String getDescription() {
return "Get general info about the bot.";
}
@Nullable
@Override
public String getUsage() {
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.TOOLS;
}
@Override @Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) { public void runCommand(MessageReceivedEvent event, String label, String[] args) {
@@ -4,8 +4,11 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache; import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat; import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand; import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections; import java.util.Collections;
@@ -28,6 +31,24 @@ public class ClearCommand implements MessageCommand
return false; return false;
} }
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
return "Clear the current channel's chat history.";
}
@Nullable
@Override
public String getUsage() {
return "[amount]";
}
@Override @Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) public void runCommand(MessageReceivedEvent event, String label, String[] args)
{ {
@@ -43,7 +64,18 @@ public class ClearCommand implements MessageCommand
// get the amount from the command args. // get the amount from the command args.
Integer toDeleteAmount; Integer toDeleteAmount;
if (args.length == 0) toDeleteAmount = 1; if (args.length == 0) toDeleteAmount = 1;
else toDeleteAmount = Integer.parseInt(args[0]); else
{
try {
toDeleteAmount = Integer.parseInt(args[0]);
} catch (NumberFormatException e)
{
toDeleteAmount = 0;
}
}
// cap the amount to avoid abuse.
if(toDeleteAmount > ClearChat.getMaxAmount()) toDeleteAmount = 0;
error = ClearChat.checkDeleteAmount(toDeleteAmount); error = ClearChat.checkDeleteAmount(toDeleteAmount);
if (error != null) { if (error != null) {
@@ -64,14 +96,16 @@ public class ClearCommand implements MessageCommand
// edit the message text and attach a button. // edit the message text and attach a button.
Button dismiss = ClearChat.getDismissButton(); Button dismiss = ClearChat.getDismissButton();
// ^ todo: maybe the dismiss button should also delete the original message sent by the user? Message finalMessage = event.getChannel().sendMessage(content).setActionRow(dismiss).complete();
// todo: but then, we need to differentiate between command type in the database, and store
// todo: that message's id too.
botMessage = botMessage.editMessage(content).setActionRow(dismiss).complete();
// add the message to database. // add the message to database.
Cache.getDatabaseSource().queueDisabling(botMessage); Cache.getDatabaseSource().queueDisabling(finalMessage);
Cache.getDatabaseSource().trackRanCommandReply(botMessage, event.getAuthor()); Cache.getDatabaseSource().trackRanCommandReply(finalMessage, event.getAuthor());
// delete the sender's message.
event.getMessage().delete().queue();
// delete the "clearing" info message.
botMessage.delete().queue();
} }
@@ -2,8 +2,10 @@ package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.CoinFlip; import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand; import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays; import java.util.Arrays;
@@ -29,6 +31,24 @@ public class CoinFlipCommand implements MessageCommand
return false; return false;
} }
@NotNull
@Override
public String getDescription() {
return "Flip a coin.";
}
@Nullable
@Override
public String getUsage() {
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@Override @Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) { public void runCommand(MessageReceivedEvent event, String label, String[] args) {
@@ -0,0 +1,73 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class DiceRollCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Arrays.asList("diceroll", "droll", "roll"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Roll dice. You can roll multiple dice at the same time." +
"\nExamples:" +
"\n - `d8 10` to roll an 8-sided die 10 times." +
"\n - `d12 3 d5 10` to roll a 12-sided die 3 times, and then a 5-sided die 10 times." +
"\n - `30` to roll a standard 6-sided die 30 times." +
"\n - `d10` to roll a 10-sided die once.";
}
@Nullable
@Override
public String getUsage() {
return "[dice size] [rolls]";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
MessageResponse response = DiceRoll.buildResponse(event.getAuthor(), args);
if(response.content() != null)
{
event.getMessage().reply(response.content()).queue();
} else if(response.embed() != null)
{
event.getMessage().replyEmbeds(response.embed()).queue();
}
}
}
@@ -2,6 +2,9 @@ package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand; import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays; import java.util.Arrays;
@@ -24,11 +27,29 @@ public class HelloCommand implements MessageCommand
return false; return false;
} }
@NotNull
@Override
public String getDescription() {
return "Get pinged by the bot.";
}
@Nullable
@Override
public String getUsage() {
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@Override @Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) public void runCommand(MessageReceivedEvent event, String label, String[] args)
{ {
String senderId = event.getMessage().getAuthor().getId(); String sender = event.getMessage().getAuthor().getAsMention();
event.getMessage().reply("Hi, <@" + senderId + ">! :sparkles:").queue(); event.getMessage().reply("Hi, " + sender + "! :sparkles:").queue();
} }
} }
@@ -0,0 +1,157 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Alias;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
public class HelpCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("help"));
}
@Nullable
@Override
public List<Permission> getPermissions() { return null; }
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Get general help on the bot. Specify a command if you want specific help about that command.";
}
@Nullable
@Override
public String getUsage() {
return "[command]";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
LinkedHashMap<CommandCategory, LinkedList<MessageCommand>> commandCategories = new LinkedHashMap<>();
if(args.length == 0)
{
for(CommandCategory category : CommandCategory.values())
{
LinkedList<MessageCommand> commandsOfThisCategory = new LinkedList<>();
for (MessageCommand command : Cache.getMessageCommandListener().getRegisteredCommands())
{
if(command.getCategory().equals(category))
{
commandsOfThisCategory.add(command);
}
}
commandCategories.put(category, commandsOfThisCategory);
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("Bot Help");
embedBuilder.addField("General Help",
"Type `" + Cache.getBotPrefix() + " help [command]` to get help on a specific command." +
"\nYou will find a list of commands organized in categories below.",
false);
for(CommandCategory category : commandCategories.keySet())
{
StringBuilder commandsList = new StringBuilder();
LinkedList<MessageCommand> commandsOfThisCategory = commandCategories.get(category);
for(int pos = 0; pos < commandsOfThisCategory.size(); pos++)
{
MessageCommand command = commandsOfThisCategory.get(pos);
commandsList.append("`").append(command.getCommandLabels().get(0)).append("`");
if(pos + 1 != commandsOfThisCategory.size())
commandsList.append(", "); // separate with comma except on last run
}
String niceCategoryName = category.name().replace("_", " ");
niceCategoryName = WordUtils.capitalizeFully(niceCategoryName);
niceCategoryName = category.getEmoji() + " " + niceCategoryName;
embedBuilder.addField(niceCategoryName, commandsList.toString(), false);
}
event.getMessage().replyEmbeds(embedBuilder.build()).queue();
} else {
String commandLabel = args[0].toLowerCase();
MessageCommand command = Cache.getMessageCommandListener().getRegisteredCommand(commandLabel);
if(command == null)
{
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
return;
}
commandLabel = command.getCommandLabels().get(0);
String usage = "`" + Cache.getBotPrefix() + " " + commandLabel;
String internalUsage = command.getUsage();
if(internalUsage != null) usage += " " + internalUsage;
usage += "`";
String aliases = Alias.generateNiceAliases(command);
List<Permission> permissions = command.getPermissions();
StringBuilder permissionsStringBuilder = new StringBuilder();
if(permissions == null)
{
permissionsStringBuilder = new StringBuilder("Available to everyone");
} else {
for(int i = 0; i < permissions.size(); i++)
{
Permission permission = permissions.get(i);
permissionsStringBuilder.append("**").append(permission.getName()).append("**");
if(i + 1 != permissions.size())
permissionsStringBuilder.append(", "); // separate with comma expect on last iteration
}
}
String title = command.getCategory().getEmoji() +
" \"" + WordUtils.capitalizeFully(commandLabel + "\" help");
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle(title);
embedBuilder.addField("Description", command.getDescription(), false);
embedBuilder.addField("Usage", usage, false);
embedBuilder.addField("Aliases", aliases, false);
embedBuilder.addField("Permissions", permissionsStringBuilder.toString(), false);
event.getMessage().replyEmbeds(embedBuilder.build()).queue();
}
}
}
@@ -5,8 +5,10 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.Invite; import wtf.beatrice.hidekobot.commands.base.Invite;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand; import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections; import java.util.Collections;
@@ -32,6 +34,24 @@ public class InviteCommand implements MessageCommand
return false; return false;
} }
@NotNull
@Override
public String getDescription() {
return "Get the bot's invite link.";
}
@Nullable
@Override
public String getUsage() {
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.MODERATION;
}
@Override @Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) public void runCommand(MessageReceivedEvent event, String label, String[] args)
{ {
@@ -49,9 +69,7 @@ public class InviteCommand implements MessageCommand
.addActionRow(inviteButton) .addActionRow(inviteButton)
.queue(); .queue();
event.getMessage().addReaction(Emoji.fromUnicode("")).queue(); event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
}, (error) -> { }, error -> event.getMessage().addReaction(Emoji.fromUnicode("")).queue());
event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
});
} else { } else {
event.getMessage() event.getMessage()
.replyEmbeds(inviteEmbed) .replyEmbeds(inviteEmbed)
@@ -0,0 +1,67 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class KickCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("kick"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return new ArrayList<Permission>(Collections.singletonList(Permission.KICK_MEMBERS));
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
return "Kick the mentioned user from the guild.";
}
@Nullable
@Override
public String getUsage() {
return "<mentioned user> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.KICK);
}
}
@@ -0,0 +1,92 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class LoveCalculatorCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("lovecalc", "lovecalculator", "lc"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; //anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Calculate how much two people love each other. You can mention two people or just one.";
}
@Nullable
@Override
public String getUsage() {
return "<person 1> [person 2]";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
Mentions mentionsObj = event.getMessage().getMentions();
List<IMentionable> mentions = mentionsObj.getMentions();
if(args.length == 0 || mentions.isEmpty())
{
event.getMessage()
.reply("\uD83D\uDE22 I need to know who to check! Please mention them.")
.queue();
return;
}
User user1, user2;
String mentionedUserId = mentions.get(0).getId();
user1 = HidekoBot.getAPI().retrieveUserById(mentionedUserId).complete();
if(mentions.size() == 1)
{
user2 = event.getAuthor();
} else {
mentionedUserId = mentions.get(1).getId();
user2 = HidekoBot.getAPI().retrieveUserById(mentionedUserId).complete();
}
MessageEmbed embed = LoveCalculator.buildEmbedAndCacheResult(event.getAuthor(), user1, user2);
event.getChannel().sendMessageEmbeds(embed).queue();
}
}
@@ -0,0 +1,74 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
import java.util.List;
public class MagicBallCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return MagicBall.getLabels();
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Ask a question to the Magic Ball.";
}
@Nullable
@Override
public String getUsage() {
return "<question>";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
if(args.length == 0)
{
event.getMessage().reply("You need to specify a question!").queue();
return;
}
StringBuilder questionBuilder = new StringBuilder();
for(int i = 0; i < args.length; i++)
{
String arg = args[i];
questionBuilder.append(arg);
if(i + 1 != args.length) // don't add a separator on the last iteration
questionBuilder.append(" ");
}
String question = questionBuilder.toString();
event.getChannel().sendMessageEmbeds(MagicBall.generateEmbed(question, event.getAuthor())).queue();
}
}
@@ -0,0 +1,69 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class SayCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("say"));
}
@Nullable
@Override
public List<Permission> getPermissions() { return Collections.singletonList(Say.getPermission()); }
@Override
public boolean passRawArgs() {
return true;
}
@NotNull
@Override
public String getDescription() {
return "Make the bot say something for you.";
}
@Nullable
@Override
public String getUsage() {
return "<text>";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String messageContent;
if(args.length != 0 && !args[0].isEmpty())
{
messageContent = args[0];
} else {
event.getMessage().reply("\uD83D\uDE20 Hey, you have to tell me what to say!")
.queue();
return;
}
event.getChannel().sendMessage(messageContent).queue();
event.getMessage().delete().queue();
}
}
@@ -0,0 +1,58 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class TimeoutCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("timeout"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return new ArrayList<Permission>(Collections.singletonList(Permission.MODERATE_MEMBERS));
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
return "Timeout the mentioned user.";
}
@Nullable
@Override
public String getUsage() {
return "<mentioned user> <duration> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.TIMEOUT);
}
}
@@ -0,0 +1,88 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class TriviaCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("trivia"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null;
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@NotNull
@Override
public String getDescription() {
return "Start a Trivia session and play with others!";
}
@Nullable
@Override
public String getUsage() {
return null;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
MessageChannel channel = event.getChannel();
if(!(channel instanceof TextChannel))
{
channel.sendMessage(Trivia.getNoDMsError()).queue();
return;
}
if(Trivia.channelsRunningTrivia.contains(channel.getId()))
{
// todo: also what if the bot stops (database...?)
// todo: also what if the message is already deleted
Message err = event.getMessage().reply(Trivia.getTriviaAlreadyRunningError()).complete();
Cache.getTaskScheduler().schedule(() -> err.delete().queue(), 10, TimeUnit.SECONDS);
return;
}
MessageResponse response = Trivia.generateMainScreen();
event.getMessage().replyEmbeds(response.embed()).addActionRow(response.components()).queue(message ->
{
Cache.getDatabaseSource().trackRanCommandReply(message, event.getAuthor());
Cache.getDatabaseSource().queueDisabling(message);
});
}
}
@@ -0,0 +1,106 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import wtf.beatrice.hidekobot.commands.base.UrbanDictionary;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class UrbanDictionaryCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return UrbanDictionary.getCommandLabels();
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; //anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@NotNull
@Override
public String getDescription() {
return "Look something up in the Urban Dictionary.";
}
@Nullable
@Override
public String getUsage() {
return "<query>";
}
@NotNull
@Override
public CommandCategory getCategory() {
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
if(args.length == 0)
{
event.getMessage().reply(UrbanDictionary.getNoArgsError()).queue();
return;
}
// sanitize args by only keeping letters and numbers, and adding "+" instead of spaces for HTML parsing
StringBuilder termBuilder = new StringBuilder();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
termBuilder.append(arg);
if(i + 1 != args.length) // add spaces between args, but not on the last run
termBuilder.append(" ");
}
String term = UrbanDictionary.sanitizeArgs(termBuilder.toString(), false);
String url = UrbanDictionary.generateUrl(term);
Document doc;
try {
doc = Jsoup.connect(url).get();
} catch (IOException e) {
event.getMessage().reply(UrbanDictionary.getTermNotFoundError()).queue();
return;
}
Elements definitions = doc.getElementsByClass("definition");
UrbanDictionary.UrbanSearch search = new UrbanDictionary.UrbanSearch(definitions);
MessageEmbed embed = UrbanDictionary.buildEmbed(term, url, event.getAuthor(), search, 0);
// disable next page if we only have one result
Button nextPageBtnLocal = UrbanDictionary.getNextPageButton();
if(search.getPages() == 1) nextPageBtnLocal = nextPageBtnLocal.asDisabled();
event.getChannel()
.sendMessageEmbeds(embed)
.addActionRow(UrbanDictionary.getPreviousPageButton().asDisabled(),
//disabled by default because we're on page 0
nextPageBtnLocal,
UrbanDictionary.getDeleteButton())
.queue(message -> UrbanDictionary.track(message, event.getAuthor(), search, term));
}
}
@@ -1,6 +1,5 @@
package wtf.beatrice.hidekobot.commands.slash; package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionMapping;
@@ -8,7 +7,8 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData; import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands; import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache; import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl; import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class AvatarCommand extends SlashCommandImpl public class AvatarCommand extends SlashCommandImpl
@@ -31,9 +31,6 @@ public class AvatarCommand extends SlashCommandImpl
User user; User user;
int resolution; int resolution;
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
OptionMapping userArg = event.getOption("user"); OptionMapping userArg = event.getOption("user");
if(userArg != null) if(userArg != null)
{ {
@@ -45,57 +42,18 @@ public class AvatarCommand extends SlashCommandImpl
OptionMapping sizeArg = event.getOption("size"); OptionMapping sizeArg = event.getOption("size");
if(sizeArg != null) if(sizeArg != null)
{ {
resolution = sizeArg.getAsInt(); resolution = ProfileImage.parseResolution(sizeArg.getAsInt());
// method to find closest value to accepted values
int distance = Math.abs(acceptedSizes[0] - resolution);
int idx = 0;
for(int c = 1; c < acceptedSizes.length; c++){
int cdistance = Math.abs(acceptedSizes[c] - resolution);
if(cdistance < distance){
idx = c;
distance = cdistance;
}
}
resolution = acceptedSizes[idx];
} else { } else {
resolution = 512; resolution = ProfileImage.parseResolution(512);
} }
EmbedBuilder embedBuilder = new EmbedBuilder(); MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if(response.content() != null)
// embed processing
{ {
embedBuilder.setColor(Cache.getBotColor()); event.getHook().editOriginal(response.content()).queue();
embedBuilder.setTitle("Profile picture"); } else if(response.embed() != null)
{
embedBuilder.addField("User", "<@" + user.getId() + ">", false); event.getHook().editOriginalEmbeds(response.embed()).queue();
embedBuilder.addField("Current resolution", resolution + " × " + resolution, false);
// string builder to create a string that links to all available resolutions
StringBuilder links = new StringBuilder();
for(int pos = 0; pos < acceptedSizes.length; pos++)
{
int currSize = acceptedSizes[pos];
String currLink = user.getEffectiveAvatar().getUrl(currSize);
links.append("[").append(currSize).append("px](").append(currLink).append(")");
if(pos + 1 != acceptedSizes.length) // don't add a separator on the last iteration
{
links.append(" | ");
}
}
embedBuilder.addField("Available resolutions", links.toString(), false);
embedBuilder.setImage(user.getEffectiveAvatar().getUrl(resolution));
} }
event.getHook().editOriginalEmbeds(embedBuilder.build()).queue();
} }
} }
@@ -0,0 +1,36 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class BanCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("ban", "Ban someone from the guild.")
.addOption(OptionType.MENTIONABLE, "target",
"The member user to ban.",
true,
false)
.addOption(OptionType.STRING, "reason",
"The reason for the punishment.",
false,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.BAN);
}
}
@@ -0,0 +1,59 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class BannerCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData() {
return Commands.slash("banner", "Get someone's profile banner.")
.addOption(OptionType.USER, "user", "User you want to grab the banner of.")
.addOption(OptionType.INTEGER, "size", "The size of the returned image.",
false,
true);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// defer reply because this might take a moment
event.deferReply().queue();
User user;
int resolution;
OptionMapping userArg = event.getOption("user");
if(userArg != null)
{
user = userArg.getAsUser();
} else {
user = event.getUser();
}
OptionMapping sizeArg = event.getOption("size");
if(sizeArg != null)
{
resolution = ProfileImage.parseResolution(sizeArg.getAsInt());
} else {
resolution = ProfileImage.parseResolution(512);
}
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
if(response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
} else if(response.embed() != null)
{
event.getHook().editOriginalEmbeds(response.embed()).queue();
}
}
}
@@ -44,6 +44,9 @@ public class ClearCommand extends SlashCommandImpl
OptionMapping amountOption = event.getOption("amount"); OptionMapping amountOption = event.getOption("amount");
int toDeleteAmount = amountOption == null ? 1 : amountOption.getAsInt(); int toDeleteAmount = amountOption == null ? 1 : amountOption.getAsInt();
// cap the amount to avoid abuse.
if(toDeleteAmount > ClearChat.getMaxAmount()) toDeleteAmount = 0;
error = ClearChat.checkDeleteAmount(toDeleteAmount); error = ClearChat.checkDeleteAmount(toDeleteAmount);
if(error != null) if(error != null)
{ {
@@ -0,0 +1,50 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class DiceRollCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("diceroll", "Roll dice. You can roll multiple dice at the same time.")
.addOption(OptionType.STRING, "query",
"The dice to roll.",
false,
false);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.deferReply().queue();
OptionMapping textOption = event.getOption("query");
String messageContent = "";
if(textOption != null)
{
messageContent = textOption.getAsString();
}
String[] args = messageContent.split("\\s");
MessageResponse response = DiceRoll.buildResponse(event.getUser(), args);
if(response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
} else if(response.embed() != null)
{
event.getHook().editOriginalEmbeds(response.embed()).queue();
}
}
}
@@ -0,0 +1,46 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class KickCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("kick", "Kick someone from the guild.")
.addOption(OptionType.MENTIONABLE, "target",
"The member user to kick.",
true,
false)
.addOption(OptionType.STRING, "reason",
"The reason for the punishment.",
false,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.KICK_MEMBERS));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.KICK);
}
}
@@ -0,0 +1,61 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class LoveCalculatorCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("lovecalc",
"Calculate how much two people love each other.")
.addOption(OptionType.MENTIONABLE,
"first",
"The first person to account for",
true)
.addOption(OptionType.MENTIONABLE,
"second",
"The second person to account for",
false);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
User firstUser, secondUser;
OptionMapping firsUserArg = event.getOption("first");
if(firsUserArg != null)
{
firstUser = firsUserArg.getAsUser(); //todo null check?
} else {
event.reply("\uD83D\uDE22 I need to know who to check! Please mention them.")
.setEphemeral(true)
.queue();
return;
}
OptionMapping secondUserArg = event.getOption("second");
if(secondUserArg != null)
{
secondUser = secondUserArg.getAsUser(); //todo null check?
} else {
secondUser = event.getUser();
}
MessageEmbed embed = LoveCalculator.buildEmbedAndCacheResult(event.getUser(), firstUser, secondUser);
event.replyEmbeds(embed).queue();
}
}
@@ -0,0 +1,49 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class MagicBallCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash(MagicBall.getLabels().get(0),
"Ask a question to the magic ball.")
.addOption(OptionType.STRING, "question",
"The question to ask.",
true,
false);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// get the asked question
OptionMapping textOption = event.getOption("question");
String question = "";
if(textOption != null)
{
question = textOption.getAsString();
}
if(textOption == null || question.isEmpty())
{
event.reply("\uD83D\uDE20 Hey, you have to ask me a question!")
.setEphemeral(true)
.queue();
return;
}
MessageEmbed response = MagicBall.generateEmbed(question, event.getUser());
event.replyEmbeds(response).queue();
}
}
@@ -1,6 +1,5 @@
package wtf.beatrice.hidekobot.commands.slash; package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
@@ -9,6 +8,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData; import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands; import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl; import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class SayCommand extends SlashCommandImpl public class SayCommand extends SlashCommandImpl
@@ -22,7 +22,7 @@ public class SayCommand extends SlashCommandImpl
"The message to send.", "The message to send.",
true, true,
false) false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MESSAGE_MANAGE)); .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Say.getPermission()));
} }
@Override @Override
@@ -0,0 +1,40 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class TimeoutCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("timeout", "Timeout someone in the guild.")
.addOption(OptionType.MENTIONABLE, "target",
"The member user to time out.",
true,
false)
.addOption(OptionType.STRING, "duration",
"The duration of the timeout.",
true,
false)
.addOption(OptionType.STRING, "reason",
"The reason for the punishment.",
false,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MODERATE_MEMBERS));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.TIMEOUT);
}
}
@@ -0,0 +1,51 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class TriviaCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("trivia",
"Start a Trivia session and play with others!");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
MessageChannel channel = event.getChannel();
if(!(channel instanceof TextChannel))
{
event.reply(Trivia.getNoDMsError()).queue();
return;
}
if(Trivia.channelsRunningTrivia.contains(channel.getId()))
{
event.reply(Trivia.getTriviaAlreadyRunningError()).setEphemeral(true).queue();
return;
}
// if we got here, this might take a bit
event.deferReply().queue();
MessageResponse response = Trivia.generateMainScreen();
event.getHook().editOriginalEmbeds(response.embed()).setActionRow(response.components()).queue(message ->
{
Cache.getDatabaseSource().trackRanCommandReply(message, event.getUser());
Cache.getDatabaseSource().queueDisabling(message);
});
}
}
@@ -0,0 +1,81 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import wtf.beatrice.hidekobot.commands.base.UrbanDictionary;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.io.IOException;
public class UrbanDictionaryCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash(UrbanDictionary.getCommandLabels().get(0),
"Look up a term on Urban Dictionary.")
.addOption(OptionType.STRING, "term", "The term to look up", true);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.deferReply().queue();
// get the term to look up
OptionMapping textOption = event.getOption("term");
String term = "";
if(textOption != null)
{
term = textOption.getAsString();
}
if(textOption == null || term.isEmpty())
{
event.reply(UrbanDictionary.getNoArgsError())
.setEphemeral(true)
.queue();
return;
}
final String sanitizedTerm = UrbanDictionary.sanitizeArgs(term, false);
String url = UrbanDictionary.generateUrl(sanitizedTerm);
Document doc;
try {
doc = Jsoup.connect(url).get();
} catch (IOException e) {
event.reply(UrbanDictionary.getTermNotFoundError())
.setEphemeral(true)
.queue();
return;
}
Elements definitions = doc.getElementsByClass("definition");
UrbanDictionary.UrbanSearch search = new UrbanDictionary.UrbanSearch(definitions);
MessageEmbed embed = UrbanDictionary.buildEmbed(sanitizedTerm, url, event.getUser(), search, 0);
// disable next page if we only have one result
Button nextPageBtnLocal = UrbanDictionary.getNextPageButton();
if(search.getPages() == 1) nextPageBtnLocal = nextPageBtnLocal.asDisabled();
ActionRow actionRow = ActionRow.of(UrbanDictionary.getPreviousPageButton().asDisabled(),
//disabled by default because we're on page 0
nextPageBtnLocal,
UrbanDictionary.getDeleteButton());
event.getHook().editOriginalEmbeds(embed).setComponents(actionRow).queue(message ->
UrbanDictionary.track(message, event.getUser(), search, sanitizedTerm));
}
}
@@ -2,6 +2,7 @@ package wtf.beatrice.hidekobot.datasources;
import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import wtf.beatrice.hidekobot.HidekoBot; import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.Logger; import wtf.beatrice.hidekobot.util.Logger;
@@ -57,7 +58,7 @@ public class ConfigurationSource
} }
} }
// load the YAML file from the filesystem // load the YAML file from the filesystem
Yaml fsConfigYaml = new Yaml(); Yaml fsConfigYaml = new Yaml(new SafeConstructor());
LinkedHashMap<String, Object> fsConfigContents = null; // map holding all file entries LinkedHashMap<String, Object> fsConfigContents = null; // map holding all file entries
try (InputStream fsConfigStream = new FileInputStream(fsConfigFile)) try (InputStream fsConfigStream = new FileInputStream(fsConfigFile))
{ fsConfigContents = fsConfigYaml.load(fsConfigStream); } { fsConfigContents = fsConfigYaml.load(fsConfigStream); }
@@ -81,6 +81,12 @@ public class DatabaseSource
* | 39402849302 | 39402849302 | 39402849302 | 39402849302 | PRIVATE | * | 39402849302 | 39402849302 | 39402849302 | 39402849302 | PRIVATE |
* -------------------------------------------------------------------------------------------- * --------------------------------------------------------------------------------------------
* *
* TABLE 3: urban_dictionary
* -----------------------------------------------------------------------------------------------------
* | message_id | page | meanings | examples | contributors | dates | term |
* -----------------------------------------------------------------------------------------------------
* | 39402849302 | 0 | base64 | base64 | base64 | base64 | miku |
* -----------------------------------------------------------------------------------------------------
*/ */
//todo: javadocs //todo: javadocs
@@ -104,6 +110,16 @@ public class DatabaseSource
"channel_type TEXT NOT NULL" + // channel type (PRIVATE, FORUM, ...) "channel_type TEXT NOT NULL" + // channel type (PRIVATE, FORUM, ...)
");"); ");");
newTables.add("CREATE TABLE IF NOT EXISTS urban_dictionary (" +
"message_id TEXT NOT NULL, " + // message id of the bot's response
"page INTEGER NOT NULL," + // page that we are currently on
"meanings TEXT NOT NULL," + // list of all meanings, serialized and encoded
"examples TEXT NOT NULL, " + // list of all examples, serialized and encoded
"contributors TEXT NOT NULL, " + // list of all contributors, serialized and encoded
"dates TEXT NOT NULL, " + // list of all submission dates, serialized and encoded
"term TEXT NOT NULL" + // the term that was searched
");");
for(String sql : newTables) for(String sql : newTables)
{ {
try (Statement stmt = dbConnection.createStatement()) { try (Statement stmt = dbConnection.createStatement()) {
@@ -301,6 +317,17 @@ public class DatabaseSource
return false; return false;
} }
query = "DELETE FROM urban_dictionary WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
preparedStatement.execute();
} catch (SQLException e)
{
e.printStackTrace();
return false;
}
return true; return true;
} }
@@ -373,5 +400,220 @@ public class DatabaseSource
return null; return null;
} }
public boolean trackUrban(String meanings, String examples,
String contributors, String dates,
Message message, String term)
{
String query = "INSERT INTO urban_dictionary " +
"(message_id, page, meanings, examples, contributors, dates, term) VALUES " +
" (?, ?, ?, ?, ?, ?, ?);";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, message.getId());
preparedStatement.setInt(2, 0);
preparedStatement.setString(3, meanings);
preparedStatement.setString(4, examples);
preparedStatement.setString(5, contributors);
preparedStatement.setString(6, dates);
preparedStatement.setString(7, term);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e)
{
e.printStackTrace();
}
return false;
}
public int getUrbanPage(String messageId)
{
String query = "SELECT page " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return 0;
while(resultSet.next())
{
return resultSet.getInt("page");
}
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
}
public String getUrbanMeanings(String messageId)
{
String query = "SELECT meanings " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("meanings");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanExamples(String messageId)
{
String query = "SELECT examples " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("examples");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanContributors(String messageId)
{
String query = "SELECT contributors " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("contributors");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanDates(String messageId)
{
String query = "SELECT dates " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("dates");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanTerm(String messageId)
{
String query = "SELECT term " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("term");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public boolean setUrbanPage(String messageId, int page)
{
String query = "UPDATE urban_dictionary " +
"SET page = ? " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setInt(1, page);
preparedStatement.setString(2, messageId);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public boolean resetExpiryTimestamp(String messageId)
{
LocalDateTime expiryTime = LocalDateTime.now().plusSeconds(Cache.getExpiryTimeSeconds());
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Cache.getExpiryTimestampFormat());
String expiryTimeFormatted = dateTimeFormatter.format(expiryTime);
String query = "UPDATE pending_disabled_messages " +
"SET expiry_timestamp = ? " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, expiryTimeFormatted);
preparedStatement.setString(2, messageId);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
} }
@@ -2,8 +2,10 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.hooks.ListenerAdapter;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.commands.base.CoinFlip; import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.commands.base.UrbanDictionary;
import wtf.beatrice.hidekobot.util.CommandUtil;
public class ButtonInteractionListener extends ListenerAdapter public class ButtonInteractionListener extends ListenerAdapter
{ {
@@ -17,8 +19,17 @@ public class ButtonInteractionListener extends ListenerAdapter
// coinflip // coinflip
case "coinflip_reflip" -> CoinFlip.buttonReFlip(event); case "coinflip_reflip" -> CoinFlip.buttonReFlip(event);
// clearchat command // generic dismiss button
case "clear_dismiss" -> ClearChat.dismissMessage(event); case "generic_dismiss" -> CommandUtil.delete(event);
// urban dictionary navigation
case "urban_nextpage" -> UrbanDictionary.changePage(event, UrbanDictionary.ChangeType.NEXT);
case "urban_previouspage" -> UrbanDictionary.changePage(event, UrbanDictionary.ChangeType.PREVIOUS);
// trivia
case "trivia_correct" -> Trivia.handleAnswer(event, Trivia.AnswerType.CORRECT);
case "trivia_wrong_1", "trivia_wrong_2", "trivia_wrong_3" ->
Trivia.handleAnswer(event, Trivia.AnswerType.WRONG);
} }
@@ -7,14 +7,13 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand; import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import wtf.beatrice.hidekobot.objects.comparators.MessageCommandAliasesComparator; import wtf.beatrice.hidekobot.objects.comparators.MessageCommandAliasesComparator;
import wtf.beatrice.hidekobot.util.Logger; import wtf.beatrice.hidekobot.util.Logger;
import java.util.Arrays; import java.util.*;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
public class MessageCommandListener extends ListenerAdapter public class MessageCommandListener extends ListenerAdapter
{ {
@@ -23,6 +22,10 @@ public class MessageCommandListener extends ListenerAdapter
private final TreeMap<LinkedList<String>, MessageCommand> registeredCommands = private final TreeMap<LinkedList<String>, MessageCommand> registeredCommands =
new TreeMap<LinkedList<String>, MessageCommand>(new MessageCommandAliasesComparator()); new TreeMap<LinkedList<String>, MessageCommand>(new MessageCommandAliasesComparator());
// map commands and their categories.
// this is not strictly needed but it's better to have it so we avoid looping every time we need to check the cat.
LinkedHashMap<CommandCategory, LinkedList<MessageCommand>> commandCategories = new LinkedHashMap<>();
private final String commandRegex = "(?i)^(hideko|hde)\\b"; private final String commandRegex = "(?i)^(hideko|hde)\\b";
// (?i) -> case insensitive flag // (?i) -> case insensitive flag
// ^ -> start of string (not in middle of a sentence) // ^ -> start of string (not in middle of a sentence)
@@ -57,10 +60,14 @@ public class MessageCommandListener extends ListenerAdapter
@Override @Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) public void onMessageReceived(@NotNull MessageReceivedEvent event)
{ {
String eventMessage = event.getMessage().getContentDisplay(); // check if a bot is sending this message, and ignore it
if(event.getAuthor().isBot()) return;
// warning: we are getting the RAW value of the message content, not the DISPLAY value!
String eventMessage = event.getMessage().getContentRaw();
// check if the sent message matches the bot activation regex (prefix, name, ...) // check if the sent message matches the bot activation regex (prefix, name, ...)
if(!eventMessage.toLowerCase().matches(commandRegex + ".*")) if(!eventMessage.toLowerCase().matches(commandRegex + "((.|\\n)*)"))
return; return;
// generate args from the string // generate args from the string
@@ -74,7 +81,9 @@ public class MessageCommandListener extends ListenerAdapter
// it will be the whole text as a single element. // it will be the whole text as a single element.
if(argsString.isEmpty()) if(argsString.isEmpty())
{ {
event.getMessage().reply("Hello there! ✨").queue(); event.getMessage()
.reply("Hello there! ✨ Type `" + Cache.getBotPrefix() + " help` to get started!")
.queue();
return; return;
} }
@@ -87,7 +96,11 @@ public class MessageCommandListener extends ListenerAdapter
if(commandObject == null) if(commandObject == null)
{ {
/* temporarily disabled because when people talk about the bot, it replies with this spammy message.
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
*/
return; return;
} }
@@ -120,8 +133,9 @@ public class MessageCommandListener extends ListenerAdapter
String[] commandArgs; String[] commandArgs;
if(commandObject.passRawArgs()) if(commandObject.passRawArgs())
{ {
// remove first argument, which is the command label // remove first argument, which is the command label
argsString = argsString.replaceAll("^[\\S]+\\s+", ""); argsString = argsString.replaceAll("^[\\S]+\\s*", "");
// pass all other arguments as a single argument as the first array element // pass all other arguments as a single argument as the first array element
commandArgs = new String[]{argsString}; commandArgs = new String[]{argsString};
} }
@@ -0,0 +1,19 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import wtf.beatrice.hidekobot.commands.base.Trivia;
public class SelectMenuInteractionListener extends ListenerAdapter
{
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event)
{
switch (event.getComponentId().toLowerCase()) {
// trivia
case "trivia_categories" -> Trivia.handleMenuSelection(event);
}
}
}
@@ -34,7 +34,7 @@ public class SlashCommandListener extends ListenerAdapter
if(command == null) return; if(command == null) return;
// finally run the command, in a new thread to avoid locking. // finally run the command, in a new thread to avoid locking the main one.
new Thread(() -> command.runSlashCommand(event)).start(); new Thread(() -> command.runSlashCommand(event)).start();
} }
} }
@@ -0,0 +1,11 @@
package wtf.beatrice.hidekobot.objects;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import org.jetbrains.annotations.Nullable;
public record MessageResponse(@Nullable String content,
@Nullable MessageEmbed embed,
@Nullable ItemComponent... components) {
}
@@ -0,0 +1,18 @@
package wtf.beatrice.hidekobot.objects.commands;
public enum CommandCategory
{
MODERATION("\uD83D\uDC40"),
FUN("\uD83C\uDFB2"),
TOOLS("\uD83D\uDEE0"),
;
private String emoji;
CommandCategory(String emoji)
{
this.emoji = emoji;
}
public String getEmoji() { return emoji; }
}
@@ -2,6 +2,7 @@ package wtf.beatrice.hidekobot.objects.commands;
import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.LinkedList; import java.util.LinkedList;
@@ -39,6 +40,31 @@ public interface MessageCommand
*/ */
boolean passRawArgs(); boolean passRawArgs();
/**
* Say what category this command belongs to.
*
* @return the command category.
*/
@NotNull
CommandCategory getCategory();
/**
* Say what this command does.
*
* @return a String explaining what this command does.
*/
@NotNull
String getDescription();
/**
* Say how people should use this command.
*
* @return a String explaining how to use the command, excluding the bot prefix and command name. Null if no parameter is needed
*/
@Nullable
String getUsage();
/** /**
* Run the command logic by parsing the event and replying accordingly. * Run the command logic by parsing the event and replying accordingly.
* *
@@ -0,0 +1,16 @@
package wtf.beatrice.hidekobot.objects.comparators;
import wtf.beatrice.hidekobot.objects.fun.TriviaCategory;
import java.util.Comparator;
/**
* This class gets two trivia categories, and compares them by their name.
*/
public class TriviaCategoryComparator implements Comparator<TriviaCategory> {
@Override
public int compare(TriviaCategory o1, TriviaCategory o2) {
return CharSequence.compare(o1.categoryName(), o2.categoryName());
}
}
@@ -0,0 +1,16 @@
package wtf.beatrice.hidekobot.objects.comparators;
import wtf.beatrice.hidekobot.objects.fun.TriviaScore;
import java.util.Comparator;
/**
* This class gets two trivia scores, and compares their score.
*/
public class TriviaScoreComparator implements Comparator<TriviaScore> {
@Override
public int compare(TriviaScore o1, TriviaScore o2) {
return Integer.compare(o2.getScore(), o1.getScore()); // inverted, because higher number should come first
}
}
@@ -0,0 +1,45 @@
package wtf.beatrice.hidekobot.objects.fun;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.UUID;
public class Dice
{
private final int sides;
private int value = 0;
private final UUID uuid;
public Dice(int sides)
{
this.sides = sides;
this.uuid = UUID.randomUUID();
}
public Dice(Dice old)
{
this.sides = old.sides;
this.value = old.value;
this.uuid = UUID.randomUUID();
}
public int getValue()
{
return value;
}
public int getSides()
{
return sides;
}
public void roll()
{
value = RandomUtil.getRandomNumber(1, sides);
}
public UUID getUUID()
{
return uuid;
}
}
@@ -0,0 +1,5 @@
package wtf.beatrice.hidekobot.objects.fun;
public record TriviaCategory(String categoryName, int categoryId) {
}
@@ -0,0 +1,8 @@
package wtf.beatrice.hidekobot.objects.fun;
import java.util.List;
public record TriviaQuestion(String question, String correctAnswer,
List<String> wrongAnswers) {
}
@@ -0,0 +1,31 @@
package wtf.beatrice.hidekobot.objects.fun;
import net.dv8tion.jda.api.entities.User;
public class TriviaScore
{
private final User user;
private int score = 0;
public TriviaScore(User user)
{
this.user = user;
}
public void changeScore(int add)
{
score += add;
}
public int getScore() { return score; }
public User getUser() { return user; }
@Override
public String toString()
{
return "[" + user.getAsTag() + "," + score + "]";
}
}
@@ -1,20 +1,12 @@
package wtf.beatrice.hidekobot.runnables; package wtf.beatrice.hidekobot.runnables;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
import net.dv8tion.jda.api.requests.RestAction;
import wtf.beatrice.hidekobot.Cache; import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.datasources.DatabaseSource; import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.util.Logger; import wtf.beatrice.hidekobot.util.Logger;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class ExpiredMessageTask implements Runnable { public class ExpiredMessageTask implements Runnable {
@@ -63,95 +55,9 @@ public class ExpiredMessageTask implements Runnable {
if(now.isAfter(expiryDate)) if(now.isAfter(expiryDate))
{ {
if(Cache.isVerbose()) logger.log("expired: " + messageId); if(Cache.isVerbose()) logger.log("expired: " + messageId);
disableExpired(messageId); CommandUtil.disableExpired(messageId);
} }
} }
}
private void disableExpired(String messageId)
{
String channelId = databaseSource.getQueuedExpiringMessageChannel(messageId);
ChannelType msgChannelType = databaseSource.getTrackedMessageChannelType(messageId);
MessageChannel textChannel = null;
// this should never happen, but only message channels are supported.
if(!msgChannelType.isMessage())
{
databaseSource.untrackExpiredMessage(messageId);
return;
}
// if this is a DM
if(!(msgChannelType.isGuild()))
{
String userId = databaseSource.getTrackedReplyUserId(messageId);
User user = HidekoBot.getAPI().retrieveUserById(userId).complete();
if(user == null)
{
// if user is not found, consider it expired
// (deleted profile, or blocked the bot)
databaseSource.untrackExpiredMessage(messageId);
return;
}
textChannel = user.openPrivateChannel().complete();
}
else
{
String guildId = databaseSource.getQueuedExpiringMessageGuild(messageId);
Guild guild = HidekoBot.getAPI().getGuildById(guildId);
if(guild == null)
{
// if guild is not found, consider it expired
// (server was deleted or bot was kicked)
databaseSource.untrackExpiredMessage(messageId);
return;
}
textChannel = guild.getTextChannelById(channelId);
}
if(textChannel == null)
{
// if channel is not found, count it as expired
// (channel was deleted or bot permissions restricted)
databaseSource.untrackExpiredMessage(messageId);
return;
}
RestAction<Message> retrieveAction = textChannel.retrieveMessageById(messageId);
if(Cache.isVerbose()) logger.log("cleaning up: " + messageId);
retrieveAction.queue(
message -> {
if(message == null)
{
databaseSource.untrackExpiredMessage(messageId);
return;
}
List<LayoutComponent> components = message.getComponents();
List<LayoutComponent> newComponents = new ArrayList<>();
for (LayoutComponent component : components)
{
component = component.asDisabled();
newComponents.add(component);
}
message.editMessageComponents(newComponents).queue();
databaseSource.untrackExpiredMessage(messageId);
},
(error) -> {
databaseSource.untrackExpiredMessage(messageId);
});
} }
} }
@@ -0,0 +1,12 @@
package wtf.beatrice.hidekobot.runnables;
import wtf.beatrice.hidekobot.Cache;
public class RandomSeedTask implements Runnable
{
@Override
public void run() {
Cache.setRandomSeed(System.currentTimeMillis());
}
}
@@ -0,0 +1,30 @@
package wtf.beatrice.hidekobot.runnables;
import net.dv8tion.jda.api.entities.Activity;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.Arrays;
import java.util.List;
public class StatusUpdateTask implements Runnable
{
List<String> statuses = Arrays.asList(
"Hatsune Miku: Project DIVA",
"Wii Sports",
"Excel",
"Mii Channel",
"Wii Speak",
"Minetest",
"Mario Kart Wii"
);
@Override
public void run() {
int randomPos = RandomUtil.getRandomNumber(0, statuses.size() - 1);
String status = statuses.get(randomPos) + " | " + Cache.getBotPrefix() + " help";
HidekoBot.getAPI().getPresence().setActivity(Activity.playing(status));
}
}
@@ -0,0 +1,202 @@
package wtf.beatrice.hidekobot.runnables;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.json.JSONObject;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.objects.comparators.TriviaScoreComparator;
import wtf.beatrice.hidekobot.objects.fun.TriviaCategory;
import wtf.beatrice.hidekobot.objects.fun.TriviaQuestion;
import wtf.beatrice.hidekobot.objects.fun.TriviaScore;
import wtf.beatrice.hidekobot.util.CommandUtil;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
public class TriviaTask implements Runnable
{
private final User author;
private final MessageChannel channel;
private Message previousMessage = null;
private final JSONObject triviaJson;
private final List<TriviaQuestion> questions;
private final TriviaCategory category;
ScheduledFuture<?> future = null;
private int iteration = 0;
public TriviaTask(User author, MessageChannel channel, TriviaCategory category)
{
this.author = author;
this.channel = channel;
this.category = category;
triviaJson = Trivia.fetchJson(Trivia.getTriviaLink(category.categoryId()));
questions = Trivia.parseQuestions(triviaJson); //todo: null check, rate limiting...
}
public void setScheduledFuture(ScheduledFuture<?> future)
{
this.future = future;
}
@Override
public void run()
{
if(previousMessage != null)
{
// todo: we shouldn't use this method, since it messes with the database... look at coin reflip
CommandUtil.disableExpired(previousMessage.getId());
String previousCorrectAnswer = questions.get(iteration-1).correctAnswer();
// we need this to be thread-locking to avoid getting out of sync with the rest of the trivia features
previousMessage.reply("The correct answer was: **" + previousCorrectAnswer + "**!").complete();
// todo: maybe also add who replied correctly as a list
// clean the list of people who answered, so they can answer again for the new question
Trivia.channelAndWhoResponded.put(previousMessage.getChannel().getId(), new ArrayList<>());
}
if(iteration >= questions.size())
{
String scoreboardText = "\uD83D\uDC23 Trivia session is over!";
List<String> winners = new ArrayList<>();
int topScore = 0;
StringBuilder othersBuilder = new StringBuilder();
LinkedList<TriviaScore> triviaScores = Trivia.channelAndScores.get(channel.getId());
if(triviaScores == null) triviaScores = new LinkedList<>();
else triviaScores.sort(new TriviaScoreComparator());
int pos = 0;
Integer previousScore = null;
for(TriviaScore triviaScore : triviaScores)
{
if(pos > 10) break; // cap at top 10
String user = triviaScore.getUser().getAsMention();
int score = triviaScore.getScore();
if(previousScore == null)
{
previousScore = score;
topScore = score;
pos = 1;
} else {
if(score != previousScore) pos++;
}
if(pos == 1) winners.add(user);
else {
othersBuilder.append("\n").append(pos)
.append(" | ").append(user)
.append(": ").append(score).append(" points");
}
}
StringBuilder winnersBuilder = new StringBuilder();
for(int i = 0; i < winners.size(); i++)
{
String winner = winners.get(i);
winnersBuilder.append(winner);
if(i + 1 != winners.size())
{
winnersBuilder.append(", "); // separate with comma except on last run
} else {
winnersBuilder.append(": ").append(topScore).append(" points \uD83C\uDF89");
}
}
String winnersTitle = "\uD83D\uDCAB ";
winnersTitle += winners.size() == 1 ? "Winner" : "Winners";
String winnersString = winnersBuilder.toString();
String othersString = othersBuilder.toString();
EmbedBuilder scoreboardBuilder = new EmbedBuilder();
scoreboardBuilder.setColor(Cache.getBotColor());
scoreboardBuilder.setTitle("\uD83C\uDF1F Trivia Scoreboard");
if(!winnersString.isEmpty()) scoreboardBuilder.addField(winnersTitle, winnersString, false);
else scoreboardBuilder.addField("\uD83D\uDE22 Sad Trivia",
"No one played \uD83D\uDE2D", false);
if(!othersString.isEmpty()) scoreboardBuilder.addField("☁️ Others", othersString, false);
channel.sendMessage(scoreboardText).addEmbeds(scoreboardBuilder.build()).queue();
// remove all cached data
Trivia.channelsRunningTrivia.remove(channel.getId());
Trivia.channelAndWhoResponded.remove(channel.getId());
Trivia.channelAndScores.remove(channel.getId());
future.cancel(false);
// we didn't implement null checks on the future on purpose, because we need to know if we were unable
// to cancel it (and console errors should make it clear enough).
return;
}
TriviaQuestion currentTriviaQuestion = questions.get(iteration);
List<Button> answerButtons = new ArrayList<>();
Button correctAnswerButton = Button.primary("trivia_correct", currentTriviaQuestion.correctAnswer());
answerButtons.add(correctAnswerButton);
int i = 0; // we need to add a number because buttons can't have the same id
for(String wrongAnswer : currentTriviaQuestion.wrongAnswers())
{
i++;
Button wrongAnswerButton = Button.primary("trivia_wrong_" + i, wrongAnswer);
answerButtons.add(wrongAnswerButton);
}
Collections.shuffle(answerButtons);
List<String> buttonEmojis = Arrays.asList("\uD83D\uDD34", "\uD83D\uDD35",
"\uD83D\uDFE2", "\uD83D\uDFE1", "\uD83D\uDFE4", "\uD83D\uDFE3", "\uD83D\uDFE0");
// add emojis to buttons
for(int emojiPos = 0; emojiPos < buttonEmojis.size(); emojiPos++)
{
if(emojiPos == answerButtons.size()) break;
String emoji = buttonEmojis.get(emojiPos);
Button button = answerButtons.get(emojiPos);
answerButtons.set(emojiPos, button.withEmoji(Emoji.fromUnicode(emoji)));
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("\uD83C\uDFB2 Trivia - " + category.categoryName() +
" (" + (iteration+1) + "/" + questions.size() + ")");
embedBuilder.addField("❓ Question", currentTriviaQuestion.question(), false);
previousMessage = channel
.sendMessageEmbeds(embedBuilder.build())
.setActionRow(answerButtons)
.complete();
Cache.getDatabaseSource().trackRanCommandReply(previousMessage, author);
// todo: ^ we should get rid of this tracking, since we don't need to know who started the trivia.
// todo: however, for now, that's the only way to avoid a thread-locking scenario as some data is
// todo: only stored in that table. this should be solved when we merge / fix the two main tables.
// todo: then, we can remove this instruction.
Cache.getDatabaseSource().queueDisabling(previousMessage);
iteration++;
}
}
@@ -0,0 +1,235 @@
package wtf.beatrice.hidekobot.util;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
import net.dv8tion.jda.api.requests.RestAction;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.ArrayList;
import java.util.List;
public class CommandUtil
{
private static final Logger logger = new Logger(CommandUtil.class);
/**
* Function to delete a message when a user clicks the "delete" button attached to that message.
* This will check in the database if that user ran the command originally.
*
* @param event the button interaction event.
*/
public static void delete(ButtonInteractionEvent event)
{
// check if the user interacting is the same one who ran the command
if (!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId()))) {
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// delete the message
event.getInteraction().getMessage().delete().queue();
// no need to manually untrack it from database, it will be purged on the next planned check.
}
/**
* Method to update slash commands registered on Discord's side.
* It runs automatically every time the bot starts, but only updates the commands in case differences
* are found, unless forced.
*
* @param force a boolean specifying if the update should be forced even if no differences were found.
*/
public static void updateSlashCommands(boolean force)
{
// populate commands list from registered commands
List<CommandData> allCommands = new ArrayList<>();
for(SlashCommand cmd : Cache.getSlashCommandListener().getRegisteredCommands())
{ allCommands.add(cmd.getSlashCommandData()); }
JDA jdaInstance = HidekoBot.getAPI();
// get all the already registered commands
List<Command> registeredCommands = jdaInstance.retrieveCommands().complete();
boolean update = false;
if(force)
{
update = true;
} else
{
// for each command that we have already registered...
for(Command currRegCmd : registeredCommands)
{
boolean found = false;
// iterate through all "recognized" commands
for(CommandData cmdData : allCommands)
{
// if we find the same command...
if(cmdData.getName().equals(currRegCmd.getName()))
{
// quit the loop since we found it.
found = true;
break;
}
}
// if no match was found, we need to send an updated command list because
// an old command was probably removed.
if(!found)
{
update = true;
// quit the loop since we only need to trigger this once.
break;
}
}
// if an update is not already queued...
if(!update)
{
// for each "recognized" valid command
for(CommandData currCmdData : allCommands)
{
boolean found = false;
// iterate through all already registered commands.
for(Command cmd : registeredCommands)
{
// if this command was already registered...
if(cmd.getName().equals(currCmdData.getName()))
{
// quit the loop since we found a match.
found = true;
break;
}
}
// if no match was found, we need to send an updated command list because
// a new command was probably added.
if(!found)
{
update = true;
// quit the loop since we only need to trigger this once.
break;
}
}
}
}
logger.log("Found " + registeredCommands.size() + " commands.");
if(update)
{
// send updated command list.
jdaInstance.updateCommands().addCommands(allCommands).queue();
logger.log("Commands updated. New total: " + allCommands.size() + ".");
}
}
/**
* Method to disable all buttons from an expired message.
*
* @param messageId the message id to disable.
*/
public static void disableExpired(String messageId)
{
DatabaseSource databaseSource = Cache.getDatabaseSource();
String channelId = databaseSource.getQueuedExpiringMessageChannel(messageId);
// todo: warning, the following method + related if check are thread-locking.
// todo: we should probably merge the two tables somehow, since they have redundant information.
ChannelType msgChannelType = databaseSource.getTrackedMessageChannelType(messageId);
MessageChannel textChannel = null;
// this should never happen, but only message channels are supported.
if(!msgChannelType.isMessage())
{
databaseSource.untrackExpiredMessage(messageId);
return;
}
// if this is a DM
if(!(msgChannelType.isGuild()))
{
String userId = databaseSource.getTrackedReplyUserId(messageId);
User user = userId == null ? null : HidekoBot.getAPI().retrieveUserById(userId).complete();
if(user == null)
{
// if user is not found, consider it expired
// (deleted profile, or blocked the bot)
databaseSource.untrackExpiredMessage(messageId);
return;
}
textChannel = user.openPrivateChannel().complete();
}
else
{
String guildId = databaseSource.getQueuedExpiringMessageGuild(messageId);
Guild guild = guildId == null ? null : HidekoBot.getAPI().getGuildById(guildId);
if(guild == null)
{
// if guild is not found, consider it expired
// (server was deleted or bot was kicked)
databaseSource.untrackExpiredMessage(messageId);
return;
}
textChannel = guild.getTextChannelById(channelId);
}
if(textChannel == null)
{
// if channel is not found, count it as expired
// (channel was deleted or bot permissions restricted)
databaseSource.untrackExpiredMessage(messageId);
return;
}
RestAction<Message> retrieveAction = textChannel.retrieveMessageById(messageId);
if(Cache.isVerbose()) logger.log("cleaning up: " + messageId);
retrieveAction.queue(
message -> {
if(message == null)
{
databaseSource.untrackExpiredMessage(messageId);
return;
}
List<LayoutComponent> components = message.getComponents();
List<LayoutComponent> newComponents = new ArrayList<>();
for (LayoutComponent component : components)
{
component = component.asDisabled();
newComponents.add(component);
}
message.editMessageComponents(newComponents).queue();
databaseSource.untrackExpiredMessage(messageId);
},
error -> databaseSource.untrackExpiredMessage(messageId));
}
}
@@ -1,29 +1,49 @@
package wtf.beatrice.hidekobot.util; package wtf.beatrice.hidekobot.util;
import wtf.beatrice.hidekobot.Cache; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class FormatUtil public class FormatUtil
{ {
/** /**
* Generate a nicely formatted uptime String that omits unnecessary data * Generate a nicely formatted time-diff String that omits unnecessary data
* (e.g. 0 days, 0 hours, 4 minutes, 32 seconds -> 4m 32s) * (e.g. 0 days, 0 hours, 4 minutes, 32 seconds -> 4m 32s)
* *
* @return the formatter String * @return the formatted String
*/ */
public static String getNiceUptime() public static String getNiceTimeDiff(LocalDateTime start)
{ {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
long uptimeSeconds = ChronoUnit.SECONDS.between(Cache.getStartupTime(), now); long uptimeSeconds = ChronoUnit.SECONDS.between(start, now);
Duration uptime = Duration.ofSeconds(uptimeSeconds); Duration uptime = Duration.ofSeconds(uptimeSeconds);
long seconds = uptime.toSecondsPart();
long minutes = uptime.toMinutesPart(); return getNiceDuration(uptime);
long hours = uptime.toHoursPart(); }
long days = uptime.toDays();
/**
* Generate a nicely formatted duration String that omits unnecessary data
* (e.g. 0 days, 0 hours, 4 minutes, 32 seconds -> 4m 32s)
*
* @return the formatted String
*/
public static String getNiceDuration(Duration duration)
{
long seconds = duration.toSecondsPart();
long minutes = duration.toMinutesPart();
long hours = duration.toHoursPart();
long days = duration.toDays();
StringBuilder uptimeStringBuilder = new StringBuilder(); StringBuilder uptimeStringBuilder = new StringBuilder();
if(days == 0) if(days == 0)
@@ -31,7 +51,7 @@ public class FormatUtil
if(hours == 0) if(hours == 0)
{ {
if(minutes == 0) if(minutes == 0)
{} else { {} else { // i know this if has an empty body but i feel like this reads better
uptimeStringBuilder.append(minutes).append("m "); uptimeStringBuilder.append(minutes).append("m ");
} }
} else { } else {
@@ -47,4 +67,81 @@ public class FormatUtil
return uptimeStringBuilder.toString(); return uptimeStringBuilder.toString();
} }
/**
* Method to parse a string into a duration.
* Warning: this only supports up to days; months and longer timeframes are unsupported.
*
* @param duration the String to parse.
* @return a Duration of the parsed timeframe, or null if parsing failed.
*/
@Nullable
public static Duration parseDuration(String duration)
{
// sanitize a bit to avoid cluttering with garbled strings
if(duration.length() > 16) duration = duration.substring(0, 16);
duration = duration.replaceAll("[^\\w]", ""); //only keep digits and word characters
duration = duration.toLowerCase();
/* the following regex matches any number followed by any amount of characters, any amount of times.
eg: 3d, 33hours, 32048dojg, 3d2h5m22s.
it does not match if the [digits and characters] blocks are missing.
eg: 33, asd, 3g5hj, 4 asd.
*/
if(!duration.matches("(\\d+[a-zA-Z]+)+"))
return null;
String[] durationTimes = duration.split("[a-zA-Z]+");
String[] durationUnits = duration.split("\\d+");
// remove first element, because it will always be empty (there's nothing before the first character)
durationUnits = Arrays.copyOfRange(durationUnits, 1, durationUnits.length);
Duration fullDuration = Duration.ZERO;
for(int i = 0; i < durationTimes.length; i++)
{
String durationTimeStr = durationTimes[i];
String durationUnitStr = durationUnits[i];
int durationValue = Integer.parseInt(durationTimeStr);
TemporalUnit unit = parseTimeUnit(durationUnitStr);
if(unit != null)
fullDuration = fullDuration.plus(durationValue, unit);
else return null; // if we failed finding the time unit, instantly quit with failed parsing.
}
return fullDuration;
}
@Nullable
private static TemporalUnit parseTimeUnit(@NotNull String unitName)
{
// we won't do any sanitization, because this is a private method, and
// we are only accessing it with things that we know for sure are already sanitized.
unitName = unitName.toLowerCase();
TemporalUnit timeUnit = null;
/*
parsing table
s, se, sec, second, seconds -> SECOND
m, mi, min, minute, minutes -> MINUTE
h, ho, hr, hour, hours -> HOUR
d, day, days -> DAY
(months and longer timeframes are unsupported due to Discord restrictions)
*/
switch (unitName)
{
case "s", "se", "sec", "second", "seconds" -> timeUnit = ChronoUnit.SECONDS;
case "m", "mi", "min", "minute", "minutes" -> timeUnit = ChronoUnit.MINUTES;
case "h", "ho", "hr", "hour", "hours" -> timeUnit = ChronoUnit.HOURS;
case "d", "day", "days" -> timeUnit = ChronoUnit.DAYS;
}
return timeUnit;
}
} }
@@ -1,12 +1,9 @@
package wtf.beatrice.hidekobot.util; package wtf.beatrice.hidekobot.util;
import java.util.Random; import wtf.beatrice.hidekobot.Cache;
public class RandomUtil public class RandomUtil
{ {
private static final Random random = new Random();
/** /**
* Returns a random integer picked in a range. * Returns a random integer picked in a range.
* *
@@ -29,7 +26,7 @@ public class RandomUtil
int difference = (max - min) + 1; int difference = (max - min) + 1;
// find a number between 0 and our range (eg. 5 -> 8 = 0 -> 3) // find a number between 0 and our range (eg. 5 -> 8 = 0 -> 3)
int randomTemp = random.nextInt(difference); int randomTemp = Cache.getRandom().nextInt(difference);
// add the minimum value, so we are sure to be in the original range (0->5, 1->6, 2->7, 3->8) // add the minimum value, so we are sure to be in the original range (0->5, 1->6, 2->7, 3->8)
return randomTemp + min; return randomTemp + min;
@@ -0,0 +1,39 @@
package wtf.beatrice.hidekobot.util;
import org.apache.commons.lang3.SerializationException;
import java.io.*;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
public class SerializationUtil
{
public static String serializeBase64(List dataList) {
try (ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream so = new ObjectOutputStream(bo)) {
so.writeObject(dataList);
so.flush();
return Base64.getEncoder().encodeToString(bo.toByteArray());
}
catch (IOException e) {
throw new SerializationException("Error during serialization", e);
}
}
public static LinkedList deserializeBase64(String dataStr) {
byte[] b = Base64.getDecoder().decode(dataStr);
ByteArrayInputStream bi = new ByteArrayInputStream(b);
ObjectInputStream si;
try {
si = new ObjectInputStream(bi);
return LinkedList.class.cast(si.readObject());
}
catch (IOException | ClassNotFoundException e) {
throw new SerializationException("Error during deserialization", e);
}
}
}
@@ -1,110 +0,0 @@
package wtf.beatrice.hidekobot.util;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.ArrayList;
import java.util.List;
public class SlashCommandUtil
{
private static final Logger logger = new Logger(MessageCommandListener.class);
public static void updateSlashCommands(boolean force)
{
// populate commands list from registered commands
List<CommandData> allCommands = new ArrayList<>();
for(SlashCommand cmd : Cache.getSlashCommandListener().getRegisteredCommands())
{ allCommands.add(cmd.getSlashCommandData()); }
JDA jdaInstance = HidekoBot.getAPI();
// get all the already registered commands
List<Command> registeredCommands = jdaInstance.retrieveCommands().complete();
boolean update = false;
if(force)
{
update = true;
} else
{
// for each command that we have already registered...
for(Command currRegCmd : registeredCommands)
{
boolean found = false;
// iterate through all "recognized" commands
for(CommandData cmdData : allCommands)
{
// if we find the same command...
if(cmdData.getName().equals(currRegCmd.getName()))
{
// quit the loop since we found it.
found = true;
break;
}
}
// if no match was found, we need to send an updated command list because
// an old command was probably removed.
if(!found)
{
update = true;
// quit the loop since we only need to trigger this once.
break;
}
}
// if an update is not already queued...
if(!update)
{
// for each "recognized" valid command
for(CommandData currCmdData : allCommands)
{
boolean found = false;
// iterate through all already registered commands.
for(Command cmd : registeredCommands)
{
// if this command was already registered...
if(cmd.getName().equals(currCmdData.getName()))
{
// quit the loop since we found a match.
found = true;
break;
}
}
// if no match was found, we need to send an updated command list because
// a new command was probably added.
if(!found)
{
update = true;
// quit the loop since we only need to trigger this once.
break;
}
}
}
}
logger.log("Found " + registeredCommands.size() + " commands.");
if(update)
{
// send updated command list.
jdaInstance.updateCommands().addCommands(allCommands).queue();
logger.log("Commands updated. New total: " + allCommands.size() + ".");
}
}
}