15 Commits

Author SHA1 Message Date
Lorenzo Dellacà
495f164552 Ignore bots interacting with hideko
All checks were successful
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
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-19 16:47:49 +01:00
Lorenzo Dellacà
f5238ced89 Bump version to 0.5.3
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-19 01:41:04 +01:00
Lorenzo Dellacà
f0ee565185 Implement basic functional diceroll command
All checks were successful
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
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-19 00:22:51 +01:00
Lorenzo Dellacà
36ad728bbc Fallback to 0 instead of 1
All checks were successful
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
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-19 00:13:14 +01:00
Lorenzo Dellacà
f0004dc555 Re-register accidentally removed invite command
All checks were successful
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
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-19 00:07:02 +01:00
Lorenzo Dellacà
660e18d1f4 Bump version to 0.5.2
All checks were successful
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
All checks were successful
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
All checks were successful
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
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-18 23:47:54 +01:00
112 changed files with 1719 additions and 6259 deletions

View File

@@ -1,14 +1,14 @@
kind: template
load: java-build-deploy.yaml
data:
arch: arm64
os: linux
build_branches:
kind: pipeline
name: default
trigger:
branch:
- main
- develop
build_events:
- push
- pull_request
sonar_project_key: HidekoBot
deploy_targets:
- production
steps:
- name: build
image: maven:3-eclipse-temurin-16
commands:
- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
- mvn javadoc:javadoc
- mvn test -B

3
.gitignore vendored
View File

@@ -1,4 +1,3 @@
target/
.idea/
scripts/
*.sqlite
*.sqlite

View File

@@ -1,88 +0,0 @@
---
kind: pipeline
type: docker
name: build
platform:
os: {{ .input.os }}
arch: {{ .input.arch }}
trigger:
branch:
{{- range .input.build_branches }}
- {{ . }}
{{- end }}
event:
{{- range .input.build_events }}
- {{ . }}
{{- end }}
# Global project-specific environment variables
environment:
{{- range .input.envs }}
{{ .name }}: {{ .value }}
{{- end }}
steps:
# Test if it compiles correctly
- name: build
image: maven:3-eclipse-temurin-21
commands:
- mvn verify --no-transfer-progress -DskipTests=true -Dmaven.javadoc.skip=true -B -V
# Run unit tests
- name: test
image: maven:3-eclipse-temurin-21
commands:
- mvn test --no-transfer-progress -B -V
# Check maven dependencies
- name: dependency-check
image: owasp/dependency-check:latest
commands:
- dependency-check --scan /src --format ALL --out /src/target --nvdApiKey $NVD_API_KEY
environment:
NVD_API_KEY:
from_secret: nvd_api_key
# Run code analysis
- name: code-analysis
when:
event:
- push
image: maven:3-eclipse-temurin-21
commands:
- mvn sonar:sonar --no-transfer-progress -Dsonar.projectKey={{ .input.sonar_project_key }} -Dsonar.host.url=$SONAR_INSTANCE_URL -Dsonar.token=$SONAR_LOGIN_KEY -B -V
environment:
SONAR_INSTANCE_URL:
from_secret: sonar_instance_url
SONAR_LOGIN_KEY:
from_secret: sonar_login_key
---
kind: pipeline
type: kubernetes
name: deploy
trigger:
event:
- promote
target:
{{- range .input.deploy_targets }}
- {{ . }}
{{- end }}
# Global project-specific environment variables
environment:
{{- range .input.envs }}
{{ .name }}: {{ .value }}
{{- end }}
steps:
# Upload to Maven repository
- name: maven-deploy
image: maven:3-eclipse-temurin-21
commands:
- mvn deploy --no-transfer-progress -DskipTests=true -Dmaven.javadoc.skip=true -B -V -gs settings.xml -Dmaven.repo.username=$MAVEN_REPO_USERNAME -Dmaven.repo.password=$MAVEN_REPO_PASSWORD
environment:
MAVEN_REPO_USERNAME:
from_secret: maven_repo_username
MAVEN_REPO_PASSWORD:
from_secret: maven_repo_password

View File

@@ -1,16 +1,7 @@
# HidekoBot
[![Reliability Rating](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=reliability_rating&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
[![Maintainability Rating](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=sqale_rating&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
[![Security Rating](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=security_rating&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
[![Build Status](https://drone.beatrice.wtf/api/badges/bea/HidekoBot/status.svg)](https://drone.beatrice.wtf/bea/HidekoBot)
[![Lines of Code](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=ncloc&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
# HidekoBot
Hideko is a general-purpose Discord bot.
## Download
The latest stable version is always uploaded automatically to the [Maven repository](https://nexus.beatrice.wtf/#browse/browse:maven-releases:wtf%2Fbeatrice%2Fhidekobot%2FHidekoBot).
You can download the JAR directly by clicking [here](https://nexus.beatrice.wtf/service/rest/v1/search/assets/download?sort=version&repository=maven-releases&maven.groupId=wtf.beatrice.hidekobot&maven.artifactId=HidekoBot&maven.extension=jar).
## Startup
Download a prebuilt JAR file or build it from source, then run it with:
```bash
@@ -21,28 +12,16 @@ make the bot change its behavior.
Additionally available parameters are:
- **verbose**: log every message that the bot receives, plus additional debugging messages. Very spammy and performance heavy.
- **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).
- **refresh**: force refresh the bot's commands.
*Note: Java 21 or later is required.*
*Note: Java 16 or later is required.*
## Initial setup
Run the startup command once. The bot will generate a `config.yml` file in your current directory (`$PWD` on GNU/Linux).
Run the startup command once. The bot will generate a `config.yml` file in the directory you were when you ran it.
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
invite-link in your console. Click on the link to add your bot to any server with the correct permissions
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 your current directory. Do not delete the database file to avoid corruption and unpredictable
behavior.
# Development
## Versioning
This project uses the `x.y.z-releaseType` schema for releases.
Development builds are tagged as `x.y.z-SNAPSHOT` and sometimes pushed to the snapshots Maven repository.
Stable builds are tagged as `x.y.z` and always pushed to the releases Maven repository, by promoting the build on
[Drone](https://drone.beatrice.wtf/). Currently, promoting stable builds is a manual process.
already set-up.

183
pom.xml
View File

@@ -6,129 +6,42 @@
<groupId>wtf.beatrice.hidekobot</groupId>
<artifactId>HidekoBot</artifactId>
<version>0.9.3-SNAPSHOT</version>
<version>0.5.3</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sonar.dependencyCheck.htmlReportPath>./target/dependency-check-report.html
</sonar.dependencyCheck.htmlReportPath>
<sonar.dependencyCheck.jsonReportPath>./target/dependency-check-report.json
</sonar.dependencyCheck.jsonReportPath>
<sonar.dependencyCheck.summarize>true</sonar.dependencyCheck.summarize>
</properties>
<dependencies>
<!-- Basic JDA dependency for Discord API -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.5.1</version>
<version>5.0.0-beta.2</version>
</dependency>
<!-- JDA depends on SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.4</version>
</dependency>
<!-- Dependency used for SQLite database connections-->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.49.1.0</version>
<version>3.39.4.1</version>
</dependency>
<!-- Dependency used for YAML configuration files -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.4</version>
</dependency>
<!-- JSoup is used to parse HTML into JSON objects for better handling in Java -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.19.1</version>
</dependency>
<!-- Various String manipulation utils -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.13.1</version>
</dependency>
<!-- JSON dependency used for better parsing of JSON files -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250517</version>
</dependency>
<!-- Start Random.org dependencies -->
<dependency>
<groupId>com.github.jinahya</groupId>
<artifactId>random-org-json-rpc</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.18.0</version>
</dependency>
<!-- End Random.org dependencies -->
<!-- Unit Tests Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.4.5</version>
</dependency>
<!-- Hibernate core + SQLite dialects -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.6.1.Final</version>
</dependency>
<!-- Jakarta Persistence API (usually provided via Hibernate, but include explicitly for compile-time types) -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
<version>1.33</version>
</dependency>
</dependencies>
<!-- override dependencies to use newer versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.31.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<resources>
@@ -141,66 +54,34 @@
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>5.1.0.4751</version>
</plugin>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.1.1</version>
<configuration>
<failBuildOnCVSS>8</failBuildOnCVSS>
<!--suppress UnresolvedMavenProperty -->
<nvdApiKey>${nvdApiKey}</nvdApiKey>
<knownExploitedUrl>
https://raw.githubusercontent.com/EugenMayer/cisa-known-exploited-mirror/main/known_exploited_vulnerabilities.json
</knownExploitedUrl>
<formats>
<format>html</format>
<format>json</format>
</formats>
<suppressionFiles>
<suppressionFile>./suppressions.xml</suppressionFile>
</suppressionFiles>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.4.5</version>
<configuration>
<!-- Replace the main artifact (no classifier) -->
<classifier></classifier>
</configuration>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>repackage</goal>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>wtf.beatrice.hidekobot.HidekoBot</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>nexus-releases</id>
<url>https://nexus.beatrice.wtf/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<url>https://nexus.beatrice.wtf/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>
</project>

View File

@@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
<servers>
<server>
<id>nexus-snapshots</id>
<username>${maven.repo.username}</username>
<password>${maven.repo.password}</password>
</server>
<server>
<id>nexus-releases</id>
<username>${maven.repo.username}</username>
<password>${maven.repo.password}</password>
</server>
</servers>
</settings>

View File

@@ -1,87 +1,56 @@
package wtf.beatrice.hidekobot;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.datasources.ConfigurationEntry;
import wtf.beatrice.hidekobot.datasources.ConfigurationSource;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.datasources.PropertiesSource;
import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import wtf.beatrice.hidekobot.listeners.MessageLogger;
import wtf.beatrice.hidekobot.listeners.SlashCommandCompletionListener;
import wtf.beatrice.hidekobot.listeners.SlashCommandListener;
import wtf.beatrice.hidekobot.util.Services;
import wtf.beatrice.hidekobot.util.Logger;
import java.awt.*;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class Cache
{
private Cache()
{
throw new IllegalStateException("Utility class");
}
// todo: make this compatible with the message listener's regex
private static final String BOT_PREFIX = "hideko";
private static final Logger LOGGER = LoggerFactory.getLogger(Cache.class);
private static Services SERVICES;
public static void setServices(Services services)
{
SERVICES = services;
}
public static Services getServices()
{
return SERVICES;
}
// 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 final String botPrefix = "hideko";
private static final Logger logger = new Logger(Cache.class);
private static PropertiesSource propertiesSource = null;
private static ConfigurationSource configurationSource = null;
private static DatabaseSource databaseSource = null;
private static boolean verbose = false;
private static MessageLogger verbosityLogger = null;
private static final long BOT_MAINTAINER_ID = 979809420714332260L;
private static final String EXPIRY_TIMESTAMP_FORMAT = "yy/MM/dd HH:mm:ss";
private static final long botMaintainerId = 979809420714332260L;
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.
private static final long EXPIRY_TIME_SECONDS = 30L;
private final static long expiryTimeSeconds = 15L;
// used to count e.g. uptime
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 static final ScheduledExecutorService taskScheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources
private static final String EXEC_PATH = System.getProperty("user.dir");
private static final String BOT_NAME = "Hideko";
private final static String execPath = System.getProperty("user.dir");
private static final String botName = "Hideko";
private static SlashCommandListener slashCommandListener = null;
private static SlashCommandCompletionListener slashCommandCompletionListener = null;
private static MessageCommandListener messageCommandListener = null;
private static final String DEFAULT_INVITE_LINK =
private final static String defaultInviteLink =
"https://discord.com/api/oauth2/authorize?client_id=%userid%&scope=bot+applications.commands&permissions=8";
private static String botApplicationId = "";
// discord api returns a broken image if you don't use specific sizes (powers of 2), so we limit it to these
private static final int[] supportedAvatarResolutions = {16, 32, 64, 128, 256, 512, 1024};
private static final int[] supportedAvatarResolutions = { 16, 32, 64, 128, 256, 512, 1024 };
/**
* Get an array of all the Discord-supported avatar resolutions.
@@ -89,21 +58,14 @@ public class Cache
*
* @return array of supported resolutions.
*/
public static int[] getSupportedAvatarResolutions()
{
return supportedAvatarResolutions;
}
public static int[] getSupportedAvatarResolutions() { return supportedAvatarResolutions; }
/**
* Checks if the bot has been started with the verbose argument.
*
* @return a boolean which is true if the bot is in verbose-mode
*/
public static synchronized boolean isVerbose()
{
return verbose;
}
public static boolean isVerbose() { return verbose; }
/**
* Set the bot's verbosity status at runtime.
@@ -111,20 +73,24 @@ public class Cache
*
* @param v the verbosity boolean value
*/
public static synchronized void setVerbose(boolean v)
public static void setVerbose(boolean v)
{
verbose = v;
if (verbosityLogger != null)
if(v)
{
HidekoBot.getAPI().removeEventListener(verbosityLogger);
verbosityLogger = null;
}
if(verbosityLogger == null)
{
verbosityLogger = new MessageLogger();
}
if (v)
{
verbosityLogger = new MessageLogger();
HidekoBot.getAPI().addEventListener(verbosityLogger);
} else {
if(verbosityLogger != null)
{
HidekoBot.getAPI().removeEventListener(verbosityLogger);
verbosityLogger = null;
}
}
}
@@ -133,8 +99,7 @@ public class Cache
*
* @return a long of the account's id
*/
public static long getBotOwnerId()
{
public static long getBotOwnerId() {
return configurationSource == null ? 0L : (Long) configurationSource.getConfigValue(ConfigurationEntry.BOT_OWNER_ID);
}
@@ -144,22 +109,17 @@ public class Cache
*
* @return a String of the bot's token.
*/
public static String getBotToken()
{
public static String getBotToken() {
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.BOT_TOKEN);
}
/**
* Get the bot maintainer's profile id.
*
* @return a long of the account's id
*/
public static long getBotMaintainerId()
{
return BOT_MAINTAINER_ID;
}
public static long getBotMaintainerId() { return botMaintainerId; }
/**
* Set the bot's application id.
@@ -176,21 +136,34 @@ public class Cache
*
* @return a string of the bot's application id
*/
public static String getBotApplicationId()
{
return botApplicationId;
}
public static String getBotApplicationId() { return botApplicationId; }
/**
* Function to generate an invite link for the bot
*
* @return a string containing the invite link
*/
public static String getInviteUrl()
{
return DEFAULT_INVITE_LINK.replace("%userid%", botApplicationId);
public static String getInviteUrl() {
return defaultInviteLink.replace("%userid%", botApplicationId);
}
/**
* Set the already fully-initialized DatabaseSource instance, ready to be accessed and used.
*
* @param databaseSourceInstance the fully-initialized DatabaseSource instance.
*/
public static void setDatabaseSourceInstance(DatabaseSource databaseSourceInstance)
{
databaseSource = databaseSourceInstance;
}
/**
* Get the fully-initialized DatabaseSource instance, ready to be used.
*
* @return the DatabaseSource instance.
*/
public static @Nullable DatabaseSource getDatabaseSource() { return databaseSource; }
/**
* Set the properties source instance loaded from the JAR archive.
*
@@ -206,46 +179,25 @@ public class Cache
*
* @return the String of the DateTimeFormatter format.
*/
public static String getExpiryTimestampFormat()
{
return EXPIRY_TIMESTAMP_FORMAT;
}
public static String getExpiryTimestampFormat(){ return expiryTimestampFormat; }
/**
* Get the amount of seconds after which a message expires.
*
* @return long value of the expiry seconds.
*/
public static long getExpiryTimeSeconds()
{
return EXPIRY_TIME_SECONDS;
}
public static long getExpiryTimeSeconds() { return expiryTimeSeconds; }
public static String getBotName()
{
return BOT_NAME;
}
public static String getBotName() { return botName; };
/**
* Get the bot's version.
*
* @return a String of the bot version.
*/
public static String getBotVersion()
{
return propertiesSource.getProperty("bot.version").toLowerCase();
}
/**
* Get the bot's source code URL.
*
* @return a String containing the base URL of the repository, including a <b>trailing slash</b>.
*/
public static String getRepositoryUrl()
{
String url = propertiesSource.getProperty("repo.base_url");
return url.endsWith("/") ? url : url + "/";
public static String getBotVersion() {
return propertiesSource.getProperty("bot.version");
}
/**
@@ -253,20 +205,17 @@ public class Cache
*
* @return the Color object.
*/
public static Color getBotColor()
{
public static Color getBotColor() {
Color defaultColor = Color.PINK;
if (configurationSource == null) return defaultColor;
if(configurationSource == null) return defaultColor;
String colorName = (String) configurationSource.getConfigValue(ConfigurationEntry.BOT_COLOR);
Color color = null;
try
{
try {
Field field = Color.class.getField(colorName);
color = (Color) field.get(null);
} catch (RuntimeException | NoSuchFieldException | IllegalAccessException e)
{
LOGGER.error("Unknown color: {}", colorName);
color = (Color)field.get(null);
} catch (Exception e) {
logger.log("Unknown color: " + colorName);
}
return color == null ? defaultColor : color;
}
@@ -274,36 +223,21 @@ public class Cache
//todo javadocs
public static void setSlashCommandListener(SlashCommandListener commandListener)
{
slashCommandListener = commandListener;
}
{ slashCommandListener = commandListener; }
public static SlashCommandListener getSlashCommandListener()
{
return slashCommandListener;
}
public static SlashCommandListener getSlashCommandListener() { return slashCommandListener; }
public static void setSlashCommandCompletionListener(SlashCommandCompletionListener commandCompletionListener)
{
slashCommandCompletionListener = commandCompletionListener;
}
{ slashCommandCompletionListener = commandCompletionListener; }
public static SlashCommandCompletionListener getSlashCommandCompletionListener()
{
return slashCommandCompletionListener;
}
public static SlashCommandCompletionListener getSlashCommandCompletionListener() { return slashCommandCompletionListener; }
public static void setMessageCommandListener(MessageCommandListener commandListener)
{
messageCommandListener = commandListener;
}
{ messageCommandListener = commandListener; }
public static MessageCommandListener getMessageCommandListener()
{
return messageCommandListener;
}
public static MessageCommandListener getMessageCommandListener() { return messageCommandListener; }
/**
* Set the bot's startup time. Generally only used at boot time.
@@ -311,9 +245,7 @@ public class Cache
* @param time a LocalDateTime of the startup moment.
*/
public static void setStartupTime(LocalDateTime time)
{
startupTime = time;
}
{ startupTime = time; }
/**
@@ -321,81 +253,25 @@ public class Cache
*
* @return a LocalDateTime object of the startup instant.
*/
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);
}
//todo javadocs
public static String getExecPath()
{
return EXEC_PATH;
}
public static String getExecPath() { return execPath; }
/*private static ConfigurationSource getConfigurationSource()
{ return configurationSource; }*/
public static String getRandomOrgApiKey()
{
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.RANDOM_ORG_API_KEY);
}
public static void setConfigurationSource(ConfigurationSource configurationSource)
{
Cache.configurationSource = configurationSource;
}
{ Cache.configurationSource = configurationSource; }
/**
* Get the bot's prefix
*
* @return a String of the bot's prefix.
*/
public static String getBotPrefix()
{
return BOT_PREFIX;
}
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;
}
public static String getBotPrefix() { return botPrefix; }
}

View File

@@ -3,28 +3,23 @@ package wtf.beatrice.hidekobot;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.context.ConfigurableApplicationContext;
import wtf.beatrice.hidekobot.commands.completer.ProfileImageCommandCompleter;
import wtf.beatrice.hidekobot.commands.message.*;
import sun.misc.Signal;
import wtf.beatrice.hidekobot.commands.completer.AvatarCommandCompleter;
import wtf.beatrice.hidekobot.commands.message.HelloCommand;
import wtf.beatrice.hidekobot.commands.slash.*;
import wtf.beatrice.hidekobot.datasources.ConfigurationSource;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.datasources.PropertiesSource;
import wtf.beatrice.hidekobot.listeners.*;
import wtf.beatrice.hidekobot.listeners.ButtonInteractionListener;
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.HeartBeatTask;
import wtf.beatrice.hidekobot.runnables.RandomOrgSeedTask;
import wtf.beatrice.hidekobot.runnables.StatusUpdateTask;
import wtf.beatrice.hidekobot.services.CommandService;
import wtf.beatrice.hidekobot.services.DatabaseService;
import wtf.beatrice.hidekobot.util.FormatUtil;
import wtf.beatrice.hidekobot.util.RandomUtil;
import wtf.beatrice.hidekobot.util.Services;
import wtf.beatrice.hidekobot.util.Logger;
import wtf.beatrice.hidekobot.util.SlashCommandUtil;
import java.io.File;
import java.time.LocalDateTime;
@@ -35,52 +30,39 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class HidekoBot
{
private static JDA jda;
private static final Logger LOGGER = LoggerFactory.getLogger(HidekoBot.class);
private static final Logger logger = new Logger(HidekoBot.class);
public static void main(String[] args)
{
// load configuration
LOGGER.info("Loading configuration...");
logger.log("Loading configuration...");
String configFilePath = Cache.getExecPath() + File.separator + "config.yml";
ConfigurationSource configurationSource = new ConfigurationSource(configFilePath);
configurationSource.initConfig();
Cache.setConfigurationSource(configurationSource);
LOGGER.info("Configuration loaded!");
logger.log("Configuration loaded!");
// load properties
LOGGER.info("Loading properties...");
logger.log("Loading properties...");
PropertiesSource propertiesSource = new PropertiesSource();
propertiesSource.load();
Cache.setPropertiesSourceInstance(propertiesSource);
LOGGER.info("Properties loaded!");
logger.log("Properties loaded!");
// check loaded bot token
String botToken = Cache.getBotToken();
if (botToken == null || botToken.isEmpty())
if(botToken == null || botToken.isEmpty())
{
LOGGER.error("Invalid bot token!");
logger.log("Invalid bot token!");
shutdown();
return;
}
ApplicationHome home = new ApplicationHome(HidekoBot.class);
System.setProperty("APP_HOME", home.getDir().getAbsolutePath());
ConfigurableApplicationContext context = SpringApplication.run(HidekoBot.class, args);
CommandService commandService = context.getBean(CommandService.class);
DatabaseService databaseService = context.getBean(DatabaseService.class);
Services services = new wtf.beatrice.hidekobot.util.Services(
commandService,
databaseService
);
Cache.setServices(services);
try
{
// try to create the bot object and authenticate it with discord.
@@ -94,14 +76,9 @@ public class HidekoBot
);
jda = jdaBuilder.build().awaitReady();
} catch (InterruptedException e)
{
LOGGER.error(e.getMessage()); // print the error message, omit the stack trace.
Thread.currentThread().interrupt(); // send interrupt to the thread.
shutdown(); // if we failed connecting and authenticating, then quit.
} catch (Exception e)
{
LOGGER.error(e.getMessage()); // print the error message, omit the stack trace.
logger.log(e.getMessage()); // print the error message, omit the stack trace.
shutdown(); // if we failed connecting and authenticating, then quit.
}
@@ -112,142 +89,108 @@ public class HidekoBot
// store if we have to force refresh commands despite no apparent changes.
boolean forceUpdateCommands = false;
// if there is at least one arg, then iterate through them because we have additional things to do.
// if there is more than 1 arg, then iterate through them because we have additional things to do.
// we are doing this at the end because we might need the API to be already initialized for some things.
if (args.length > 0)
{
if(args.length > 1) {
List<String> argsList = new ArrayList<>(Arrays.asList(args));
// NOTE: do not replace with enhanced for, since we might need
// to know what position we're at or do further elaboration of the string.
// we were using this for api key parsing in the past.
for (int i = 0; i < argsList.size(); i++)
for(int i = 0; i < argsList.size(); i++)
{
String arg = argsList.get(i);
if (arg.equals("verbose")) Cache.setVerbose(true);
if (arg.equals("refresh")) forceUpdateCommands = true;
if(arg.equals("verbose")) Cache.setVerbose(true);
if(arg.equals("refresh")) forceUpdateCommands = true;
}
}
boolean enableRandomSeedUpdaterTask = false;
// initialize random.org object if API key is provided
{
if (RandomUtil.isRandomOrgKeyValid())
{
LOGGER.info("Enabling Random.org integration... This might take a while!");
RandomUtil.initRandomOrg();
enableRandomSeedUpdaterTask = true;
LOGGER.info("Random.org integration enabled!");
}
}
// register slash commands and completers
SlashCommandListener slashCommandListener = context.getBean(SlashCommandListener.class);
SlashCommandCompletionListener slashCommandCompletionListener = context.getBean(SlashCommandCompletionListener.class);
MessageCommandListener messageCommandListener = context.getBean(MessageCommandListener.class);
ButtonInteractionListener buttonInteractionListener = context.getBean(ButtonInteractionListener.class);
SelectMenuInteractionListener selectMenuInteractionListener = context.getBean(SelectMenuInteractionListener.class);
SlashAvatarCommand slashAvatarCommand = context.getBean(SlashAvatarCommand.class);
ProfileImageCommandCompleter avatarCommandCompleter = new ProfileImageCommandCompleter(slashAvatarCommand);
slashCommandListener.registerCommand(slashAvatarCommand);
SlashCommandListener slashCommandListener = new SlashCommandListener();
SlashCommandCompletionListener slashCommandCompletionListener = new SlashCommandCompletionListener();
AvatarCommand avatarCommand = new AvatarCommand();
AvatarCommandCompleter avatarCommandCompleter = new AvatarCommandCompleter(avatarCommand);
slashCommandListener.registerCommand(avatarCommand);
slashCommandCompletionListener.registerCommandCompleter(avatarCommandCompleter);
slashCommandListener.registerCommand(context.getBean(SlashBanCommand.class));
SlashBannerCommand slashBannerCommand = context.getBean(SlashBannerCommand.class);
ProfileImageCommandCompleter bannerCommandCompleter = new ProfileImageCommandCompleter(slashBannerCommand);
slashCommandListener.registerCommand(slashBannerCommand);
slashCommandCompletionListener.registerCommandCompleter(bannerCommandCompleter);
slashCommandListener.registerCommand(context.getBean(SlashBotInfoCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashClearCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashCoinFlipCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashDiceRollCommand.class));
slashCommandListener.registerCommand(new SlashDieCommand());
slashCommandListener.registerCommand(new SlashHelpCommand());
slashCommandListener.registerCommand(context.getBean(SlashInviteCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashKickCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashLoveCalculatorCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashMagicBallCommand.class));
slashCommandListener.registerCommand(new SlashPingCommand());
slashCommandListener.registerCommand(context.getBean(SlashSayCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashTimeoutCommand.class));
slashCommandListener.registerCommand(new SlashTriviaCommand());
slashCommandListener.registerCommand(new SlashUrbanDictionaryCommand());
// register message commands
messageCommandListener.registerCommand(new MessageHelloCommand());
messageCommandListener.registerCommand(context.getBean(MessageAliasCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageAvatarCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageBanCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageBannerCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageBotInfoCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageCoinFlipCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageClearCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageDiceRollCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageHelpCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageInviteCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageKickCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageLoveCalculatorCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageMagicBallCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageSayCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageTimeoutCommand.class));
messageCommandListener.registerCommand(new MessageTriviaCommand());
messageCommandListener.registerCommand(new MessageUrbanDictionaryCommand());
// register listeners
slashCommandListener.registerCommand(new BotInfoCommand());
slashCommandListener.registerCommand(new ClearCommand());
slashCommandListener.registerCommand(new CoinFlipCommand());
slashCommandListener.registerCommand(new DieCommand());
slashCommandListener.registerCommand(new HelpCommand());
slashCommandListener.registerCommand(new InviteCommand());
slashCommandListener.registerCommand(new PingCommand());
slashCommandListener.registerCommand(new SayCommand());
Cache.setSlashCommandListener(slashCommandListener);
Cache.setSlashCommandCompletionListener(slashCommandCompletionListener);
// register message commands
MessageCommandListener messageCommandListener = new MessageCommandListener();
messageCommandListener.registerCommand(new HelloCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.AvatarCommand());
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.ClearCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.DiceRollCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.InviteCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.SayCommand());
Cache.setMessageCommandListener(messageCommandListener);
// register listeners
jda.addEventListener(messageCommandListener);
jda.addEventListener(slashCommandListener);
jda.addEventListener(slashCommandCompletionListener);
jda.addEventListener(buttonInteractionListener);
jda.addEventListener(selectMenuInteractionListener);
jda.addEventListener(new ButtonInteractionListener());
// update slash commands (delayed)
final boolean finalForceUpdateCommands = forceUpdateCommands;
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.schedule(() -> commandService.updateSlashCommands(finalForceUpdateCommands),
1, TimeUnit.SECONDS);
}
Executors.newSingleThreadScheduledExecutor().schedule(() -> // todo: try-with-resources
SlashCommandUtil.updateSlashCommands(finalForceUpdateCommands), 1, TimeUnit.SECONDS);
// set the bot's status
jda.getPresence().setStatus(OnlineStatus.ONLINE);
jda.getPresence().setActivity(Activity.playing("Hatsune Miku: Project DIVA"));
// start scheduled runnables
ScheduledExecutorService scheduler = Cache.getTaskScheduler();
ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask(services.databaseService(), services.commandService());
scheduler.scheduleAtFixedRate(expiredMessageTask, 5L, 5L, TimeUnit.SECONDS); //every 5 seconds
HeartBeatTask heartBeatTask = new HeartBeatTask();
scheduler.scheduleAtFixedRate(heartBeatTask, 10L, 30L, TimeUnit.SECONDS); //every 30 seconds
StatusUpdateTask statusUpdateTask = new StatusUpdateTask();
scheduler.scheduleAtFixedRate(statusUpdateTask, 0L, 60L * 5L, TimeUnit.SECONDS); // every 5 minutes
if (enableRandomSeedUpdaterTask)
// connect to database
logger.log("Connecting to database...");
String dbFilePath = Cache.getExecPath() + File.separator + "db.sqlite"; // in current directory
DatabaseSource databaseSource = new DatabaseSource(dbFilePath);
if(databaseSource.connect() && databaseSource.initDb())
{
RandomOrgSeedTask randomSeedTask = new RandomOrgSeedTask();
scheduler.scheduleAtFixedRate(randomSeedTask, 15L, 15L, TimeUnit.MINUTES); // every 15 minutes
logger.log("Database connection initialized!");
Cache.setDatabaseSourceInstance(databaseSource);
// load data here...
logger.log("Database data loaded into memory!");
} else {
logger.log("Error initializing database connection!");
}
// start scheduled runnables
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources
ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask();
scheduler.scheduleAtFixedRate(expiredMessageTask, 5, 5, TimeUnit.SECONDS); //every 5 seconds
HeartBeatTask heartBeatTask = new HeartBeatTask();
scheduler.scheduleAtFixedRate(heartBeatTask, 10, 30, TimeUnit.SECONDS); //every 30 seconds
// register shutdown interrupt signal listener for proper shutdown.
Runtime.getRuntime().addShutdownHook(new Thread(HidekoBot::preShutdown));
Signal.handle(new Signal("INT"), signal -> shutdown());
// set startup time.
Cache.setStartupTime(LocalDateTime.now());
// print the bot logo.
LOGGER.info("\n\n{}\nv{} - bot is ready!\n", FormatUtil.getLogo(), Cache.getBotVersion());
logger.log("\n\n" + logger.getLogo() + "\nv" + Cache.getBotVersion() + " - bot is ready!\n", 2);
// log the invite-link to console so noob users can just click on it.
LOGGER.info("Bot User ID: {}", botUserId);
LOGGER.info("Invite Link: {}", Cache.getInviteUrl());
logger.log("Bot User ID: " + botUserId, 3);
logger.log("Invite Link: " + Cache.getInviteUrl(), 4);
}
public static JDA getAPI()
{
return jda;
@@ -255,14 +198,9 @@ public class HidekoBot
public static void shutdown()
{
preShutdown();
logger.log("WARNING! Shutting down!");
if(jda != null) jda.shutdown();
System.exit(0);
}
private static void preShutdown()
{
LOGGER.warn("WARNING! Shutting down!");
if (jda != null) jda.shutdown();
}
}

View File

@@ -1,26 +0,0 @@
package wtf.beatrice.hidekobot.commands.base;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
@Component
public class Alias
{
public 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();
}
}

View File

@@ -0,0 +1,62 @@
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;
public class Avatar
{
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 MessageEmbed buildEmbed(int resolution, User user)
{
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("Profile picture");
embedBuilder.addField("User", "<@" + user.getId() + ">", false);
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));
return embedBuilder.build();
}
}

View File

@@ -2,21 +2,17 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.FormatUtil;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
import java.util.List;
@Component
public class BotInfo
{
public MessageEmbed generateEmbed(List<String> commandLabels)
public static MessageEmbed generateEmbed(List<String> commandLabels)
{
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
@@ -24,7 +20,7 @@ public class BotInfo
// thumbnail
String botAvatarUrl = HidekoBot.getAPI().getSelfUser().getAvatarUrl();
if (botAvatarUrl != null) embedBuilder.setThumbnail(botAvatarUrl);
if(botAvatarUrl != null) embedBuilder.setThumbnail(botAvatarUrl);
// help field
long ownerId = Cache.getBotOwnerId();
@@ -36,12 +32,12 @@ public class BotInfo
// type-specific commands list field
StringBuilder commandsListBuilder = new StringBuilder();
commandsListBuilder.append(commandLabels.size()).append(" total - ");
for (int i = 0; i < commandLabels.size(); i++)
commandsListBuilder.append(commandLabels.size()).append( " total - ");
for(int i = 0; i < commandLabels.size(); i++)
{
commandsListBuilder.append("`").append(commandLabels.get(i)).append("`");
if (i + 1 != commandLabels.size()) // don't add comma in last iteration
if(i + 1 != commandLabels.size()) // don't add comma in last iteration
{
commandsListBuilder.append(", ");
}
@@ -52,38 +48,25 @@ public class BotInfo
// keep track of how many total commands we have
int commandsCount = 0;
// message commands info field
String messageCommandsInfo;
if (Cache.getMessageCommandListener() == null)
messageCommandsInfo = "❌ disabled";
else
{
messageCommandsInfo = "✅ available";
// message commands info fields
StringBuilder messageCommandsInfoBuilder = new StringBuilder();
if(Cache.getMessageCommandListener() == null)
messageCommandsInfoBuilder.append("❌ disabled");
else {
messageCommandsInfoBuilder.append("✅ available");
commandsCount += Cache.getMessageCommandListener().getRegisteredCommands().size();
}
embedBuilder.addField("Message commands", messageCommandsInfo, true);
embedBuilder.addField("Message commands", messageCommandsInfoBuilder.toString(), true);
// slash commands info field
String slashCommandsInfo;
if (Cache.getMessageCommandListener() == null)
slashCommandsInfo = "❌ disabled";
else
{
slashCommandsInfo = "✅ available";
// slash commands info fields
StringBuilder slashCommandsInfoBuilder = new StringBuilder();
if(Cache.getMessageCommandListener() == null)
slashCommandsInfoBuilder.append("❌ disabled");
else {
slashCommandsInfoBuilder.append("✅ available");
commandsCount += Cache.getSlashCommandListener().getRegisteredCommands().size();
}
embedBuilder.addField("Slash commands", slashCommandsInfo, true);
// random.org integration field
String randomOrgInfo;
if (RandomUtil.isRandomOrgKeyValid())
{
randomOrgInfo = "✅ connected";
} else
{
randomOrgInfo = "❌ disabled";
}
embedBuilder.addField("Random.org", randomOrgInfo, true);
embedBuilder.addField("Slash commands", slashCommandsInfoBuilder.toString(), true);
// commands count fields
embedBuilder.addField("Total commands", "Loaded: `" + commandsCount + "`", true);
@@ -108,18 +91,12 @@ public class BotInfo
embedBuilder.addField("Maintainer", developerMention, true);
// uptime field
embedBuilder.addField("Uptime", FormatUtil.getNiceTimeDiff(Cache.getStartupTime()), true);
embedBuilder.addField("Uptime", FormatUtil.getNiceUptime(), true);
// issue tracker field
String link = "[Issue tracker](" + Cache.getRepositoryUrl() + "issues)";
embedBuilder.addField("Support",
link, true);
// bot birthday field
embedBuilder.addField("Bot age",
Cache.getBotName() + " was created " + FormatUtil.getNiceTimeDiff(Cache.getBotBirthDate()) + "ago!",
false);
"[Issue tracker](https://git.beatrice.wtf/mind-overflow/HidekoBot/issues)",
true); //todo: we should probably make this a final field in the config class
return embedBuilder.build();
}

View File

@@ -3,63 +3,58 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageHistory;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.Channel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import java.util.ArrayList;
import java.util.List;
@Component
public class ClearChat
{
public String getLabel()
{
public static String getLabel() {
return "clear";
}
public String getDescription()
{
public static String getDescription() {
return "Clear the current channel's chat.";
}
public Permission getPermission()
{
public static Permission getPermission() {
return Permission.MESSAGE_MANAGE;
}
public String checkDMs(Channel channel)
public static String checkDMs(Channel channel)
{
if (!(channel instanceof TextChannel))
{
return "\uD83D\uDE22 Sorry! I can't delete messages here.";
}
if(!(channel instanceof TextChannel))
{ return "\uD83D\uDE22 Sorry! I can't delete messages here."; }
return null;
}
public String checkDeleteAmount(int toDeleteAmount)
public static String checkDeleteAmount(int toDeleteAmount)
{
if (toDeleteAmount <= 0)
{
return "\uD83D\uDE22 Sorry, I can't delete that amount of messages!";
}
if(toDeleteAmount <= 0)
{ return "\uD83D\uDE22 Sorry, I can't delete that amount of messages!"; }
return null;
}
public 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 deleted = 0;
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.
int limit = 95; //discord limits this method to range 2-100. we set it to 95 to be safe.
// 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.
@@ -70,7 +65,7 @@ public class ClearChat
//if there are some messages left, but less than <limit>, we need one more iterations.
int remainder = toDeleteAmount % limit;
if (remainder != 0) iterations++;
if(remainder != 0) iterations++;
// set the starting point.
long messageId = startingMessageId;
@@ -79,70 +74,70 @@ public class ClearChat
boolean outOfBounds = false;
// do iterate.
for (int iteration = 0; iteration < iterations; iteration++)
for(int iteration = 0; iteration < iterations; iteration++)
{
if (outOfBounds) break;
if(outOfBounds) break;
// set how many messages to delete for this iteration (usually <limit> unless there's a remainder)
int iterationSize = limit;
// if we are at the last iteration... check if we have <limit> or fewer messages to delete
if (iteration + 1 == iterations && remainder != 0)
// if we are at the last iteration...
if(iteration+1 == iterations)
{
iterationSize = remainder;
// check if we have <limit> or fewer messages to delete
if(remainder != 0) iterationSize = remainder;
}
if (iterationSize == 1)
if(iterationSize == 1)
{
// grab the message
Message toDelete = channel.retrieveMessageById(messageId).complete();
//only delete one message
if (toDelete != null) toDelete.delete().queue();
if(toDelete != null) toDelete.delete().queue();
else outOfBounds = true;
// increase deleted counter by 1
deleted++;
} else
{
} else {
// get the last <iterationSize - 1> messages.
MessageHistory.MessageRetrieveAction action = channel.getHistoryBefore(messageId, iterationSize - 1);
// note: first one is the most recent, last one is the oldest message.
List<Message> messages = new ArrayList<>();
// (we are skipping first iteration since it would return an error, given that the id is the slash command and not a message)
if (iteration != 0) messages.add(channel.retrieveMessageById(messageId).complete());
if(iteration!=0) messages.add(channel.retrieveMessageById(messageId).complete());
messages.addAll(action.complete().getRetrievedHistory());
// check if we only have one or zero messages left (trying to delete more than possible)
if (messages.size() <= 1)
if(messages.size() <= 1)
{
outOfBounds = true;
} else
{
} else {
// before deleting, we need to grab the <previous to the oldest> message's id for next iteration.
action = channel.getHistoryBefore(messages.getLast().getIdLong(), 1);
action = channel.getHistoryBefore(messages.get(messages.size() - 1).getIdLong(), 1);
List<Message> previousMessage = action.complete().getRetrievedHistory();
// if that message exists (we are not out of bounds)... store it
if (!previousMessage.isEmpty()) messageId = previousMessage.getFirst().getIdLong();
if(!previousMessage.isEmpty()) messageId = previousMessage.get(0).getIdLong();
else outOfBounds = true;
}
// queue messages for deletion
if (messages.size() == 1)
if(messages.size() == 1)
{
messages.getFirst().delete().queue();
} else if (!messages.isEmpty())
messages.get(0).delete().queue();
}
else if(!messages.isEmpty())
{
try
{
try {
((TextChannel) channel).deleteMessages(messages).complete();
/* alternatively, we could use purgeMessages, which is smarter...
however, it also tries to delete messages older than 2 weeks
which are restricted by discord, and thus has to use
a less efficient way that triggers rate-limiting very quickly. */
} catch (RuntimeException ignored)
} catch (Exception e)
{
return -1;
}
}
@@ -155,31 +150,47 @@ public class ClearChat
return deleted;
}
public Button getDismissButton()
public static Button getDismissButton()
{
return Button.primary("generic_dismiss", "Dismiss")
return Button.primary("clear_dismiss", "Dismiss")
.withEmoji(Emoji.fromUnicode(""));
}
public String parseAmount(int deleted)
public static String parseAmount(int deleted)
{
if (deleted < 1)
if(deleted < 1)
{
return "\uD83D\uDE22 Couldn't clear any message!";
} else if (deleted == 1)
} else if(deleted == 1)
{
return "✂ Cleared 1 message!";
} else
{
} else {
return "✂ Cleared " + deleted + " messages!";
}
}
// cap the amount to avoid abuse.
public int getMaxAmount()
private void respond(Object responseFlowObj, String content)
{
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();
}
}
}

View File

@@ -6,85 +6,64 @@ 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.buttons.Button;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.ArrayList;
import java.util.List;
@Component
public class CoinFlip
{
public Button getReflipButton()
{
public static Button getReflipButton() {
return Button.primary("coinflip_reflip", "Flip again")
.withEmoji(Emoji.fromUnicode("\uD83E\uDE99"));
.withEmoji(Emoji.fromFormatted("\uD83E\uDE99"));
}
public String genRandom()
public static String genRandom()
{
int rand = RandomUtil.getRandomNumber(0, 1);
String msg;
if (rand == 1)
if(rand == 1)
{
msg = ":coin: It's **Heads**!";
} else
{
} else {
msg = "It's **Tails**! :coin:";
}
return msg;
}
public void buttonReFlip(ButtonInteractionEvent event)
public static void buttonReFlip(ButtonInteractionEvent event)
{
// Ack ASAP to avoid 3s timeout
event.deferEdit().queue(hook -> {
// Permission check **after** ack
if (!Cache.getServices().databaseService().isUserTrackedFor(event.getUser().getId(), event.getMessageId()))
{
hook.sendMessage("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// 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;
}
// Disable all components on the original message
List<ActionRow> oldRows = event.getMessage().getActionRows();
List<ActionRow> disabledRows = new ArrayList<>(oldRows.size());
for (ActionRow row : oldRows)
{
disabledRows.add(row.asDisabled());
}
hook.editOriginalComponents(disabledRows).queue();
// set old message's button as disabled
List<ActionRow> actionRows = event.getMessage().getActionRows();
actionRows.set(0, actionRows.get(0).asDisabled());
event.editComponents(actionRows).queue();
// Send a follow-up with a fresh button
hook.sendMessage(genRandom())
.addActionRow(getReflipButton())
.queue(msg -> trackAndRestrict(msg, event.getUser()), err -> {
});
}, failure -> {
// Rare: if we couldn't ack, try best-effort fallbacks
try
{
List<ActionRow> oldRows = event.getMessage().getActionRows();
List<ActionRow> disabledRows = new ArrayList<>(oldRows.size());
for (ActionRow row : oldRows) disabledRows.add(row.asDisabled());
event.getMessage().editMessageComponents(disabledRows).queue();
} catch (Exception ignored)
{
}
event.getChannel().sendMessage(genRandom())
.addActionRow(getReflipButton())
.queue(msg -> trackAndRestrict(msg, event.getUser()), err -> {
});
});
// perform coin flip
event.getHook().sendMessage(genRandom())
.addActionRow(getReflipButton())
.queue((message) ->
{
// set the command as expiring and restrict it to the user who ran it
trackAndRestrict(message, event.getUser());
}, (error) -> {});
}
public void trackAndRestrict(Message replyMessage, User user)
public static void trackAndRestrict(Message replyMessage, User user)
{
Cache.getServices().databaseService().queueDisabling(replyMessage);
Cache.getServices().databaseService().trackRanCommandReply(replyMessage, user);
String replyMessageId = replyMessage.getId();
Cache.getDatabaseSource().queueDisabling(replyMessage);
Cache.getDatabaseSource().trackRanCommandReply(replyMessage, user);
}
}

View File

@@ -1,171 +0,0 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.fun.Dice;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.UUID;
@Component
public class DiceRoll
{
public MessageResponse buildResponse(User author, String[] args)
{
LinkedHashMap<Dice, Integer> dicesToRoll = new LinkedHashMap<>();
String diceRegex = "d\\d+";
String amountRegex = "\\d+";
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 (Map.Entry<Dice, Integer> entry : dicesToRoll.entrySet())
{
Dice dice = entry.getKey();
Integer rollsToMake = entry.getValue();
for (int roll = 0; roll < rollsToMake; roll++)
{
dice.roll();
rolledDices.add(new Dice(dice));
}
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getName(), null, author.getAvatarUrl());
embedBuilder.setTitle("Dice Roll");
if (RandomUtil.isRandomOrgKeyValid())
embedBuilder.setFooter("Seed provided by random.org");
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());
}
}

View File

@@ -4,15 +4,13 @@ import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
@Component
public class Invite
{
public MessageEmbed generateEmbed()
public static MessageEmbed generateEmbed()
{
EmbedBuilder embedBuilder = new EmbedBuilder();
@@ -20,7 +18,7 @@ public class Invite
{
embedBuilder.setColor(Cache.getBotColor());
String avatarUrl = HidekoBot.getAPI().getSelfUser().getAvatarUrl();
if (avatarUrl != null) embedBuilder.setThumbnail(avatarUrl);
if(avatarUrl != null) embedBuilder.setThumbnail(avatarUrl);
embedBuilder.setTitle("Invite");
embedBuilder.appendDescription("Click on the button below to invite " +
Cache.getBotName() +
@@ -30,7 +28,7 @@ public class Invite
return embedBuilder.build();
}
public Button getInviteButton()
public static Button getInviteButton()
{
String inviteUrl = Cache.getInviteUrl();
return Button.link(inviteUrl, "Invite " + Cache.getBotName())

View File

@@ -1,51 +0,0 @@
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.concurrent.TimeUnit;
@Component
public class LoveCalculator
{
public 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.getName(), 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();
}
}

View File

@@ -1,69 +0,0 @@
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 org.springframework.stereotype.Component;
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;
@Component
public class MagicBall
{
public LinkedList<String> getLabels()
{
return new LinkedList<>(Arrays.asList("8ball", "8b", "eightball", "magicball"));
}
private final 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 String getRandomAnswer()
{
int answerPos = RandomUtil.getRandomNumber(0, answers.size() - 1);
return answers.get(answerPos);
}
public 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.getName(), 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();
}
}

View File

@@ -1,110 +0,0 @@
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
@Component
public class ProfileImage
{
public 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 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;
}
}

View File

@@ -1,13 +1,11 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.Permission;
import org.springframework.stereotype.Component;
@Component
public class Say
{
public Permission getPermission()
{
public static Permission getPermission() {
return Permission.MESSAGE_MANAGE;
}
}

View File

@@ -1,329 +0,0 @@
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 org.slf4j.LoggerFactory;
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 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.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class Trivia
{
private Trivia()
{
throw new IllegalStateException("Utility class");
}
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Trivia.class);
private static final String TRIVIA_API_LINK = "https://opentdb.com/api.php?amount=10&type=multiple&category=";
private static final String TRIVIA_API_CATEGORIES_LINK = "https://opentdb.com/api_category.php";
public static List<String> channelsRunningTrivia = Collections.synchronizedList(new ArrayList<>());
// first string is the channelId, the list contain all users who responded there
public static ConcurrentHashMap<String, List<String>> channelAndWhoResponded = new ConcurrentHashMap<>();
// first string is the channelId, the list contain all score records for that channel
public static ConcurrentHashMap<String, LinkedList<TriviaScore>> channelAndScores = new ConcurrentHashMap<>();
public static String getTriviaLink(int categoryId)
{
return TRIVIA_API_LINK + categoryId;
}
public static String getCategoriesLink()
{
return TRIVIA_API_CATEGORIES_LINK;
}
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()
{
JSONObject categoriesJson = Trivia.fetchJson(Trivia.getCategoriesLink());
if (categoriesJson == null)
return new MessageResponse("Error fetching trivia!", null); // todo nicer with emojis
List<TriviaCategory> categories = Trivia.parseCategories(categoriesJson);
if (categories.isEmpty())
return new MessageResponse("Error parsing trivia categories!", null); // todo nicer with emojis
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)
{
LOGGER.error("JSON Parsing Exception", e);
}
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)
{
// Ack immediately with an ephemeral deferral to avoid 3s timeout
event.deferReply(true).queue(hook -> {
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))
{
// Public message in channel
event.getChannel().sendMessage(user.getAsMention() + " got it right! \uD83E\uDD73 (**+3**)").queue();
currentUserScore.changeScore(3);
} else
{
event.getChannel().sendMessage("" + user.getAsMention() + ", that's not the right answer! (**-1**)").queue();
currentUserScore.changeScore(-1);
}
scores.add(currentUserScore);
channelAndScores.put(channelId, scores);
} else
{
// Show the warning **in the original ephemeral message**, then delete it after 5s.
hook.editOriginal("☹️ " + user.getAsMention() + ", you can't answer twice!").queue(v ->
hook.deleteOriginal().queueAfter(3, TimeUnit.SECONDS, null, __ -> {
})
);
return; // don't run the generic cleanup below; we want the message visible for ~5s
}
// Clean up the ephemeral deferral (no visible ephemeral message left) for the normal path
hook.deleteOriginal().queue(null, __ -> {
});
}, __ -> {
});
}
private static synchronized 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)
{
// Ack immediately (ephemeral) so we can safely do DB/work
event.deferReply(true).queue(hook -> {
// check if the user interacting is the same one who ran the command
if (!(Cache.getServices().databaseService().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
{
hook.sendMessage("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// Disable buttons on the original message via service (uses separate REST calls)
Cache.getServices().commandService().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);
// remove the ephemeral deferral to keep things clean
hook.deleteOriginal().queue(null, __ -> {
});
}, __ -> {
});
}
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()))
{
// Already running: inform ephemerally via hook (the interaction was deferred in the caller)
event.getHook().sendMessage(Trivia.getTriviaAlreadyRunningError())
.setEphemeral(true)
.queue(msg -> Cache.getTaskScheduler().schedule(() -> msg.delete().queue(), 10, TimeUnit.SECONDS));
return;
} else
{
// Public info that a new session is starting
channel.sendMessage("Starting new Trivia session!").queue();
}
TriviaTask triviaTask = new TriviaTask(author, channel, category,
Cache.getServices().databaseService(), Cache.getServices().commandService());
ScheduledFuture<?> future =
Cache.getTaskScheduler().scheduleAtFixedRate(triviaTask,
0,
15,
TimeUnit.SECONDS);
triviaTask.setScheduledFuture(future);
Trivia.channelsRunningTrivia.add(channel.getId());
}
public enum AnswerType
{
CORRECT, WRONG
}
}

View File

@@ -1,347 +0,0 @@
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.services.DatabaseService;
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
{
private UrbanDictionary()
{
throw new IllegalStateException("Utility class");
}
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.getName(), 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.getServices().databaseService().queueDisabling(message);
Cache.getServices().databaseService().trackRanCommandReply(message, user);
Cache.getServices().databaseService().trackUrban(search.getSerializedMeanings(),
search.getSerializedExamples(),
search.getSerializedContributors(),
search.getSerializedDates(),
message,
sanitizedTerm);
}
public static void changePage(ButtonInteractionEvent event, ChangeType changeType)
{
event.deferEdit().queue();
String messageId = event.getMessageId();
DatabaseService database = Cache.getServices().databaseService();
// 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 = database.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.getHook().editOriginalEmbeds(updatedEmbed)
.setComponents(currentRow)
.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;
}
}

View File

@@ -1,279 +0,0 @@
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 org.springframework.stereotype.Component;
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.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class UserPunishment
{
private static final Duration maxTimeoutDuration = Duration.of(28, ChronoUnit.DAYS);
private static final Duration minTimeoutDuration = Duration.of(30, ChronoUnit.SECONDS);
public 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 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 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;
boolean impossible = false;
try
{
switch (punishmentType)
{
case BAN -> punishmentAction = guild.ban(mentioned, 0, TimeUnit.SECONDS);
case KICK -> punishmentAction = guild.kick(mentioned);
case TIMEOUT ->
{
// Ensure a duration argument is provided at index 1 (after mention/user)
if (args == null || args.length <= 1)
{
return new MessageResponse("Please specify a punishment duration!", 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 (!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)
{
impossible = true;
}
if (punishmentAction == null)
impossible = true;
if (impossible)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, I couldn't " + punishmentTypeName + " " + mentioned.getAsMention() + "!",
null);
}
if (!reason.isEmpty() && !reasonBuilder.isEmpty())
punishmentAction.reason("[" + author.getName() + "] " + 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.getName(), 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;
}
}
}

View File

@@ -10,28 +10,26 @@ import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.ArrayList;
import java.util.List;
public class ProfileImageCommandCompleter extends SlashArgumentsCompleterImpl
public class AvatarCommandCompleter extends SlashArgumentsCompleterImpl
{
public ProfileImageCommandCompleter(SlashCommand parentCommand)
{
public AvatarCommandCompleter(SlashCommand parentCommand) {
super(parentCommand);
}
@Override
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event)
{
if (event.getFocusedOption().getName().equals("size"))
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event) {
if(event.getFocusedOption().getName().equals("size"))
{
List<Command.Choice> options = new ArrayList<>();
for (int res : Cache.getSupportedAvatarResolutions())
for(int res : Cache.getSupportedAvatarResolutions())
{
String resString = String.valueOf(res);
String userInput = event.getFocusedOption().getValue();
if (resString.startsWith(userInput))
if(resString.startsWith(userInput))
options.add(new Command.Choice(resString, res));
}

View File

@@ -0,0 +1,81 @@
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.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.Avatar;
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;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
User user = null;
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 = Avatar.parseResolution(givenRes);
resFound = true;
break;
} catch (NumberFormatException ignored) {
}
}
// fallback in case we didn't find any specified resolution
if(!resFound) resolution = Avatar.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
MessageEmbed embed = Avatar.buildEmbed(resolution, user);
event.getMessage().replyEmbeds(embed).queue();
}
}

View File

@@ -0,0 +1,48 @@
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 org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BotInfoCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("botinfo"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
return false;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) {
// get a list of message commands
LinkedList<MessageCommand> messageCommands = Cache.getMessageCommandListener().getRegisteredCommands();
LinkedList<String> commandNames = new LinkedList<>();
for (MessageCommand command : messageCommands) {
commandNames.add(command.getCommandLabels().get(0));
}
// send the list
MessageEmbed embed = BotInfo.generateEmbed(commandNames);
event.getMessage().replyEmbeds(embed).queue();
}
}

View File

@@ -4,76 +4,38 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
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;
@Component
public class MessageClearCommand implements MessageCommand
public class ClearCommand implements MessageCommand
{
private final ClearChat clearChat;
public MessageClearCommand(@Autowired ClearChat clearChat)
{
this.clearChat = clearChat;
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList(ClearChat.getLabel()));
}
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList(clearChat.getLabel()));
}
public List<Permission> getPermissions() { return Collections.singletonList(ClearChat.getPermission()); }
@Override
public List<Permission> getPermissions()
{
return Collections.singletonList(clearChat.getPermission());
}
@Override
public boolean passRawArgs()
{
public boolean passRawArgs() {
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
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String senderId = event.getMessage().getAuthor().getId();
// check if user is trying to run command in dms.
String error = clearChat.checkDMs(event.getChannel());
if (error != null)
{
String error = ClearChat.checkDMs(event.getChannel());
if (error != null) {
event.getMessage().reply(error).queue();
return;
}
@@ -83,8 +45,7 @@ public class MessageClearCommand implements MessageCommand
if (args.length == 0) toDeleteAmount = 1;
else
{
try
{
try {
toDeleteAmount = Integer.parseInt(args[0]);
} catch (NumberFormatException e)
{
@@ -92,12 +53,8 @@ public class MessageClearCommand implements MessageCommand
}
}
// cap the amount to avoid abuse.
if (toDeleteAmount > clearChat.getMaxAmount()) toDeleteAmount = 0;
error = clearChat.checkDeleteAmount(toDeleteAmount);
if (error != null)
{
error = ClearChat.checkDeleteAmount(toDeleteAmount);
if (error != null) {
event.getMessage().reply(error).queue();
return;
}
@@ -106,20 +63,20 @@ public class MessageClearCommand implements MessageCommand
String content = "\uD83D\uDEA7 Clearing...";
Message botMessage = event.getMessage().reply(content).complete();
int deleted = clearChat.delete(toDeleteAmount,
int deleted = ClearChat.delete(toDeleteAmount,
event.getMessageIdLong(),
event.getChannel());
// get a nicely formatted message that logs the deletion of messages.
content = clearChat.parseAmount(deleted);
content = ClearChat.parseAmount(deleted);
// edit the message text and attach a button.
Button dismiss = clearChat.getDismissButton();
Button dismiss = ClearChat.getDismissButton();
Message finalMessage = event.getChannel().sendMessage(content).setActionRow(dismiss).complete();
// add the message to database.
Cache.getServices().databaseService().queueDisabling(finalMessage);
Cache.getServices().databaseService().trackRanCommandReply(finalMessage, event.getAuthor());
Cache.getDatabaseSource().queueDisabling(finalMessage);
Cache.getDatabaseSource().trackRanCommandReply(finalMessage, event.getAuthor());
// delete the sender's message.
event.getMessage().delete().queue();

View File

@@ -0,0 +1,44 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class CoinFlipCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Arrays.asList("coinflip", "flip", "flipcoin"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
return null; // null because it can be used anywhere
}
@Override
public boolean passRawArgs() {
return false;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) {
// perform coin flip
event.getMessage().reply(CoinFlip.genRandom())
.addActionRow(CoinFlip.getReflipButton())
.queue((message) ->
{
// set the command as expiring and restrict it to the user who ran it
CoinFlip.trackAndRestrict(message, event.getAuthor());
}, (error) -> {});
}
}

View File

@@ -0,0 +1,128 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.objects.Dice;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.*;
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;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
LinkedHashMap<Dice, Integer> dicesToRoll = new LinkedHashMap<Dice, Integer>();
String diceRegex = "d[0-9]+";
String amountRegex = "[0-9]+";
Dice currentDice = null;
int currentAmount;
UUID lastPushedDice = null;
for(String arg : args)
{
if(arg.matches(amountRegex))
{
currentAmount = Integer.parseInt(arg);
if(currentDice == null)
{
currentDice = new Dice(6);
} else {
currentDice = new Dice(currentDice);
}
lastPushedDice = currentDice.getUUID();
dicesToRoll.put(currentDice, currentAmount);
}
else if(arg.matches(diceRegex))
{
int sides = Integer.parseInt(arg.substring(1));
if(args.length == 1)
{
dicesToRoll.put(new Dice(sides), 1);
} else
{
if(currentDice != null)
{
if(lastPushedDice == null || !lastPushedDice.equals(currentDice.getUUID()))
{
dicesToRoll.put(currentDice, 1);
lastPushedDice = currentDice.getUUID();
}
}
currentDice = new Dice(sides);
}
}
}
if(lastPushedDice == null)
{
if(currentDice != null)
{
dicesToRoll.put(currentDice, 1);
}
} else
{
if(!lastPushedDice.equals(currentDice.getUUID()))
{
dicesToRoll.put(new Dice(currentDice), 1);
}
}
LinkedList<Dice> rolledDices = new LinkedList<>();
for(Dice dice : dicesToRoll.keySet())
{
for(int roll = 0; roll < dicesToRoll.get(dice); roll++)
{
dice.roll();
rolledDices.add(new Dice(dice));
}
}
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(dice.getValue());
total += dice.getValue();
}
message.append("\n\n").append("**Total**: ").append(total);
event.getMessage().reply(message.toString()).queue();
}
}

View File

@@ -0,0 +1,34 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class HelloCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Arrays.asList("hi", "hello", "heya"));
}
@Override
public List<Permission> getPermissions() { return null; }
@Override
public boolean passRawArgs() {
return false;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String senderId = event.getMessage().getAuthor().getId();
event.getMessage().reply("Hi, <@" + senderId + ">! :sparkles:").queue();
}
}

View File

@@ -5,78 +5,43 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.emoji.Emoji;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Invite;
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;
@Component
public class MessageInviteCommand implements MessageCommand
public class InviteCommand implements MessageCommand
{
private final Invite invite;
public MessageInviteCommand(@Autowired Invite invite)
{
this.invite = invite;
}
@Override
public LinkedList<String> getCommandLabels()
{
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("invite"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
public List<Permission> getPermissions() {
return null;
}
@Override
public boolean passRawArgs()
{
public boolean passRawArgs() {
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
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
MessageEmbed inviteEmbed = invite.generateEmbed();
Button inviteButton = invite.getInviteButton();
MessageEmbed inviteEmbed = Invite.generateEmbed();
Button inviteButton = Invite.getInviteButton();
// if this is a guild, don't spam the invite in public but DM it
if (event.getChannelType().isGuild())
if(event.getChannelType().isGuild())
{
event.getAuthor().openPrivateChannel().queue(privateChannel ->
{
@@ -84,9 +49,10 @@ public class MessageInviteCommand implements MessageCommand
.addActionRow(inviteButton)
.queue();
event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
}, error -> event.getMessage().addReaction(Emoji.fromUnicode("")).queue());
} else
{
}, (error) -> {
event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
});
} else {
event.getMessage()
.replyEmbeds(inviteEmbed)
.addActionRow(inviteButton)

View File

@@ -1,94 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageAliasCommand implements MessageCommand
{
private final Alias alias;
public MessageAliasCommand(@Autowired Alias alias)
{
this.alias = alias;
}
@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();
}
}

View File

@@ -1,122 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageAvatarCommand implements MessageCommand
{
private final ProfileImage profileImage;
public MessageAvatarCommand(@Autowired ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@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)
{
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)
{
// ignored because we're running a check after this block
}
}
// 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();
}
}
}

View File

@@ -1,73 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageBanCommand implements MessageCommand
{
private final UserPunishment userPunishment;
public MessageBanCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@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);
}
}

View File

@@ -1,122 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageBannerCommand implements MessageCommand
{
private final ProfileImage profileImage;
public MessageBannerCommand(@Autowired ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@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)
{
// ignored because we're running a check after this block
}
}
// 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();
}
}
}

View File

@@ -1,86 +0,0 @@
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo;
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;
@Component
public class MessageBotInfoCommand implements MessageCommand
{
private final BotInfo botInfo;
public MessageBotInfoCommand(@Autowired BotInfo botInfo)
{
this.botInfo = botInfo;
}
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("botinfo", "info"));
}
@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 general info about the bot.";
}
@Nullable
@Override
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
// get a list of message commands
LinkedList<MessageCommand> messageCommands = Cache.getMessageCommandListener().getRegisteredCommands();
LinkedList<String> commandNames = new LinkedList<>();
for (MessageCommand command : messageCommands)
{
commandNames.add(command.getCommandLabels().get(0));
}
// send the list
MessageEmbed embed = botInfo.generateEmbed(commandNames);
event.getMessage().replyEmbeds(embed).queue();
}
}

View File

@@ -1,82 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.CoinFlip;
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;
@Component
public class MessageCoinFlipCommand implements MessageCommand
{
private final CoinFlip coinFlip;
public MessageCoinFlipCommand(@Autowired CoinFlip coinFlip)
{
this.coinFlip = coinFlip;
}
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("coinflip", "flip", "flipcoin"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // null because it can be used anywhere
}
@Override
public boolean passRawArgs()
{
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
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
// perform coin flip
event.getMessage().reply(coinFlip.genRandom())
.addActionRow(coinFlip.getReflipButton())
.queue((message) ->
{
// set the command as expiring and restrict it to the user who ran it
coinFlip.trackAndRestrict(message, event.getAuthor());
}, (error) -> {
});
}
}

View File

@@ -1,91 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageDiceRollCommand implements MessageCommand
{
private final DiceRoll diceRoll;
public MessageDiceRollCommand(@Autowired DiceRoll diceRoll)
{
this.diceRoll = diceRoll;
}
@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.
Examples:
- `d8 10` to roll an 8-sided die 10 times.
- `d12 3 d5 10` to roll a 12-sided die 3 times, and then a 5-sided die 10 times.
- `30` to roll a standard 6-sided die 30 times.
- `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();
}
}
}

View File

@@ -1,65 +0,0 @@
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 org.springframework.stereotype.Component;
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;
@Component
public class MessageHelloCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("hi", "hello", "heya"));
}
@Override
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs()
{
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
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String sender = event.getMessage().getAuthor().getAsMention();
event.getMessage().reply("Hi, " + sender + "! :sparkles:").queue();
}
}

View File

@@ -1,174 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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.*;
@Component
public class MessageHelpCommand implements MessageCommand
{
private final Alias alias;
public MessageHelpCommand(@Autowired Alias alias)
{
this.alias = alias;
}
@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 (Map.Entry<CommandCategory, LinkedList<MessageCommand>> entry : commandCategories.entrySet())
{
StringBuilder commandsList = new StringBuilder();
CommandCategory category = entry.getKey();
LinkedList<MessageCommand> commandsOfThisCategory = entry.getValue();
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();
}
}
}

View File

@@ -1,73 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageKickCommand implements MessageCommand
{
private final UserPunishment userPunishment;
public MessageKickCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@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);
}
}

View File

@@ -1,107 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageLoveCalculatorCommand implements MessageCommand
{
private final LoveCalculator loveCalculator;
public MessageLoveCalculatorCommand(@Autowired LoveCalculator loveCalculator)
{
this.loveCalculator = loveCalculator;
}
@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();
}
}

View File

@@ -1,89 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageMagicBallCommand implements MessageCommand
{
private final MagicBall magicBall;
public MessageMagicBallCommand(@Autowired MagicBall magicBall)
{
this.magicBall = magicBall;
}
@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();
}
}

View File

@@ -1,96 +0,0 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageSayCommand implements MessageCommand
{
private final Say say;
public MessageSayCommand(@Autowired Say say)
{
this.say = say;
}
@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();
if (event.getChannel() instanceof TextChannel)
{
event.getMessage().delete().queue(response -> {
// nothing to do with the response
}, error -> {
// ignore the error if we couldn't delete it, we were probably missing permissions
// without this block it would print a stack trace in console
});
}
}
}

View File

@@ -1,73 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
@Component
public class MessageTimeoutCommand implements MessageCommand
{
private final UserPunishment userPunishment;
public MessageTimeoutCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@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);
}
}

View File

@@ -1,104 +0,0 @@
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 net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
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 MessageTriviaCommand 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();
Message recvMessage = event.getMessage();
MessageCreateAction responseAction = null;
if (response.content() != null) responseAction = recvMessage.reply(response.content());
else if (response.embed() != null) responseAction = recvMessage.replyEmbeds(response.embed());
if (responseAction != null)
{
if (response.components() != null) responseAction = responseAction.addActionRow(response.components());
responseAction.queue(message -> {
Cache.getServices().databaseService().trackRanCommandReply(message, event.getAuthor());
Cache.getServices().databaseService().queueDisabling(message);
});
}
}
}

View File

@@ -1,115 +0,0 @@
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 MessageUrbanDictionaryCommand 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));
}
}

View File

@@ -0,0 +1,51 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.commands.base.Say;
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;
}
@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();
}
}

View File

@@ -1,5 +1,7 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.EmbedBuilder;
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;
@@ -7,24 +9,14 @@ 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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Avatar;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashAvatarCommand extends SlashCommandImpl
public class AvatarCommand extends SlashCommandImpl
{
private final ProfileImage profileImage;
public SlashAvatarCommand(@NotNull ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@Override
public CommandData getSlashCommandData()
{
public CommandData getSlashCommandData() {
return Commands.slash("avatar", "Get someone's profile picture.")
.addOption(OptionType.USER, "user", "User you want to grab the avatar of.")
.addOption(OptionType.INTEGER, "size", "The size of the returned image.",
@@ -42,30 +34,22 @@ public class SlashAvatarCommand extends SlashCommandImpl
int resolution;
OptionMapping userArg = event.getOption("user");
if (userArg != null)
if(userArg != null)
{
user = userArg.getAsUser();
} else
{
} else {
user = event.getUser();
}
OptionMapping sizeArg = event.getOption("size");
if (sizeArg != null)
if(sizeArg != null)
{
resolution = profileImage.parseResolution(sizeArg.getAsInt());
} else
{
resolution = profileImage.parseResolution(512);
resolution = Avatar.parseResolution(sizeArg.getAsInt());
} else {
resolution = Avatar.parseResolution(512);
}
MessageResponse response = profileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if (response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
} else if (response.embed() != null)
{
event.getHook().editOriginalEmbeds(response.embed()).queue();
}
MessageEmbed embed = Avatar.buildEmbed(resolution, user);
event.getHook().editOriginalEmbeds(embed).queue();
}
}

View File

@@ -5,8 +5,6 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
@@ -15,20 +13,10 @@ import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.LinkedList;
import java.util.List;
@Component
public class SlashBotInfoCommand extends SlashCommandImpl
public class BotInfoCommand extends SlashCommandImpl
{
private final BotInfo botInfo;
public SlashBotInfoCommand(@Autowired BotInfo botInfo)
{
this.botInfo = botInfo;
}
@Override
public CommandData getSlashCommandData()
{
public CommandData getSlashCommandData() {
return Commands.slash("botinfo", "Get info about the bot.");
}
@@ -41,14 +29,14 @@ public class SlashBotInfoCommand extends SlashCommandImpl
// get a list of slash commands
List<SlashCommand> registeredCommands = Cache.getSlashCommandListener().getRegisteredCommands();
LinkedList<String> registeredCommandNames = new LinkedList<>();
for (SlashCommand command : registeredCommands)
for(SlashCommand command : registeredCommands)
{
// node: adding slash so people realize that this is specific about slash commands.
registeredCommandNames.add("/" + command.getCommandName());
}
// send the list
MessageEmbed embed = botInfo.generateEmbed(registeredCommandNames);
MessageEmbed embed = BotInfo.generateEmbed(registeredCommandNames);
event.getHook().editOriginalEmbeds(embed).queue();
}
}

View File

@@ -9,29 +9,19 @@ 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.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashClearCommand extends SlashCommandImpl
public class ClearCommand extends SlashCommandImpl
{
private final ClearChat clearChat;
public SlashClearCommand(@Autowired ClearChat clearChat)
{
this.clearChat = clearChat;
}
@Override
public CommandData getSlashCommandData()
{
return Commands.slash(clearChat.getLabel(),
clearChat.getDescription())
public CommandData getSlashCommandData() {
return Commands.slash(ClearChat.getLabel(),
ClearChat.getDescription())
.addOption(OptionType.INTEGER, "amount", "The amount of messages to delete.")
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(clearChat.getPermission()));
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(ClearChat.getPermission()));
}
@Override
@@ -41,8 +31,8 @@ public class SlashClearCommand extends SlashCommandImpl
event.deferReply().queue();
// check if user is trying to run command in dms.
String error = clearChat.checkDMs(event.getChannel());
if (error != null)
String error = ClearChat.checkDMs(event.getChannel());
if(error != null)
{
event.getHook().editOriginal(error).queue();
return;
@@ -54,11 +44,8 @@ public class SlashClearCommand extends SlashCommandImpl
OptionMapping amountOption = event.getOption("amount");
int toDeleteAmount = amountOption == null ? 1 : amountOption.getAsInt();
// cap the amount to avoid abuse.
if (toDeleteAmount > clearChat.getMaxAmount()) toDeleteAmount = 0;
error = clearChat.checkDeleteAmount(toDeleteAmount);
if (error != null)
error = ClearChat.checkDeleteAmount(toDeleteAmount);
if(error != null)
{
event.getHook().editOriginal(error).queue();
return;
@@ -69,20 +56,20 @@ public class SlashClearCommand extends SlashCommandImpl
Message botMessage = event.getHook().editOriginal(content).complete();
// actually delete the messages.
int deleted = clearChat.delete(toDeleteAmount,
int deleted = ClearChat.delete(toDeleteAmount,
event.getInteraction().getIdLong(),
event.getChannel());
// get a nicely formatted message that logs the deletion of messages.
content = clearChat.parseAmount(deleted);
content = ClearChat.parseAmount(deleted);
// edit the message text and attach a button.
Button dismiss = clearChat.getDismissButton();
Button dismiss = ClearChat.getDismissButton();
botMessage = botMessage.editMessage(content).setActionRow(dismiss).complete();
// add the message to database.
Cache.getServices().databaseService().queueDisabling(botMessage);
Cache.getServices().databaseService().trackRanCommandReply(botMessage, event.getUser());
Cache.getDatabaseSource().queueDisabling(botMessage);
Cache.getDatabaseSource().trackRanCommandReply(botMessage, event.getUser());
}
}

View File

@@ -4,20 +4,11 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashCoinFlipCommand extends SlashCommandImpl
public class CoinFlipCommand extends SlashCommandImpl
{
private final CoinFlip coinFlip;
public SlashCoinFlipCommand(@Autowired CoinFlip coinFlip)
{
this.coinFlip = coinFlip;
}
@Override
public CommandData getSlashCommandData()
@@ -30,19 +21,20 @@ public class SlashCoinFlipCommand extends SlashCommandImpl
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// perform coin flip
event.reply(coinFlip.genRandom())
.addActionRow(coinFlip.getReflipButton())
event.reply(CoinFlip.genRandom())
.addActionRow(CoinFlip.getReflipButton())
.queue((interaction) ->
{
// set the command as expiring and restrict it to the user who ran it
interaction.retrieveOriginal().queue((message) ->
{
coinFlip.trackAndRestrict(message, event.getUser());
}, (error) -> {
});
}, (error) -> {
});
CoinFlip.trackAndRestrict(message, event.getUser());
}, (error) -> {});
}, (error) -> {});
}
}

View File

@@ -10,14 +10,12 @@ import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SlashDieCommand extends SlashCommandImpl
public class DieCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
public CommandData getSlashCommandData() {
return Commands.slash("die", "Stop the bot's process.")
.setDefaultPermissions(DefaultMemberPermissions.DISABLED);
}
@@ -25,16 +23,12 @@ public class SlashDieCommand extends SlashCommandImpl
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
if (Cache.getBotOwnerId() != event.getUser().getIdLong())
if(Cache.getBotOwnerId() != event.getUser().getIdLong())
{
event.reply("Sorry, only the bot owner can run this command!").setEphemeral(true).queue();
} else
{
} else {
event.reply("Going to sleep! Cya ✨").queue();
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.schedule(HidekoBot::shutdown, 3, TimeUnit.SECONDS);
}
Executors.newSingleThreadScheduledExecutor().schedule(HidekoBot::shutdown, 3, TimeUnit.SECONDS);
}
}
}

View File

@@ -8,7 +8,7 @@ import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class SlashHelpCommand extends SlashCommandImpl
public class HelpCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()

View File

@@ -10,19 +10,11 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Invite;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashInviteCommand extends SlashCommandImpl
public class InviteCommand extends SlashCommandImpl
{
private final Invite invite;
public SlashInviteCommand(@NotNull Invite invite)
{
this.invite = invite;
}
@Override
public CommandData getSlashCommandData()
@@ -38,14 +30,14 @@ public class SlashInviteCommand extends SlashCommandImpl
ReplyCallbackAction replyCallbackAction = event.deferReply();
// only make message permanent in DMs
if (event.getChannelType() != ChannelType.PRIVATE)
if(event.getChannelType() != ChannelType.PRIVATE)
{
replyCallbackAction = replyCallbackAction.setEphemeral(true);
}
replyCallbackAction.queue();
MessageEmbed inviteEmbed = invite.generateEmbed();
Button inviteButton = invite.getInviteButton();
MessageEmbed inviteEmbed = Invite.generateEmbed();
Button inviteButton = Invite.getInviteButton();
WebhookMessageEditAction<Message> reply =
event.getHook()

View File

@@ -6,7 +6,7 @@ import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class SlashPingCommand extends SlashCommandImpl
public class PingCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()

View File

@@ -1,5 +1,6 @@
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.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
@@ -8,20 +9,11 @@ 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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashSayCommand extends SlashCommandImpl
public class SayCommand extends SlashCommandImpl
{
private final Say say;
public SlashSayCommand(@NotNull Say say)
{
this.say = say;
}
@Override
public CommandData getSlashCommandData()
{
@@ -31,7 +23,7 @@ public class SlashSayCommand extends SlashCommandImpl
"The message to send.",
true,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(say.getPermission()));
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Say.getPermission()));
}
@Override
@@ -42,12 +34,12 @@ public class SlashSayCommand extends SlashCommandImpl
// get the text to send
OptionMapping textOption = event.getOption("text");
String messageContent = "";
if (textOption != null)
if(textOption != null)
{
messageContent = textOption.getAsString();
messageContent = textOption.getAsString();
}
if (textOption == null || messageContent.isEmpty())
if(textOption == null || messageContent.isEmpty())
{
event.reply("\uD83D\uDE20 Hey, you have to tell me what to say!")
.setEphemeral(true)

View File

@@ -1,46 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashBanCommand extends SlashCommandImpl
{
private final UserPunishment userPunishment;
public SlashBanCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@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);
}
}

View File

@@ -1,71 +0,0 @@
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashBannerCommand extends SlashCommandImpl
{
private final ProfileImage profileImage;
public SlashBannerCommand(@NotNull ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@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();
}
}
}

View File

@@ -1,59 +0,0 @@
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashDiceRollCommand extends SlashCommandImpl
{
private final DiceRoll diceRoll;
public SlashDiceRollCommand(@NotNull DiceRoll diceRoll)
{
this.diceRoll = diceRoll;
}
@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();
}
}
}

View File

@@ -1,47 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashKickCommand extends SlashCommandImpl
{
private final UserPunishment userPunishment;
public SlashKickCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@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);
}
}

View File

@@ -1,72 +0,0 @@
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashLoveCalculatorCommand extends SlashCommandImpl
{
private final LoveCalculator loveCalculator;
public SlashLoveCalculatorCommand(@NotNull LoveCalculator loveCalculator)
{
this.loveCalculator = loveCalculator;
}
@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();
}
}

View File

@@ -1,58 +0,0 @@
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 org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashMagicBallCommand extends SlashCommandImpl
{
private final MagicBall magicBall;
public SlashMagicBallCommand(@NotNull MagicBall magicBall)
{
this.magicBall = magicBall;
}
@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();
}
}

View File

@@ -1,51 +0,0 @@
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
@Component
public class SlashTimeoutCommand extends SlashCommandImpl
{
private final UserPunishment userPunishment;
public SlashTimeoutCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@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);
}
}

View File

@@ -1,51 +0,0 @@
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 SlashTriviaCommand 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.getServices().databaseService().trackRanCommandReply(message, event.getUser());
Cache.getServices().databaseService().queueDisabling(message);
});
}
}

View File

@@ -1,83 +0,0 @@
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 SlashUrbanDictionaryCommand 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));
}
}

View File

@@ -7,27 +7,18 @@ public enum ConfigurationEntry
BOT_OWNER_ID("bot-owner-id", 100000000000000000L),
BOT_COLOR("bot-color", "PINK"),
HEARTBEAT_LINK("heartbeat-link", "https://your-heartbeat-api.com/api/push/apikey?status=up&msg=OK&ping="),
RANDOM_ORG_API_KEY("random-org-api-key", "00000000-0000-0000-0000-000000000000"),
;
private String path;
private Object defaultValue;
ConfigurationEntry(String path, Object defaultValue)
{
this.path = path;
this.defaultValue = defaultValue;
}
public String getPath()
{
return path;
}
public Object getDefaultValue()
{
return defaultValue;
}
public String getPath() { return path; }
public Object getDefaultValue() { return defaultValue; }
}

View File

@@ -1,26 +1,25 @@
package wtf.beatrice.hidekobot.datasources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.Logger;
import java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
public class ConfigurationSource
{
private final LinkedHashMap<String, Object> configurationEntries = new LinkedHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationSource.class);
private final Logger logger;
private final String configFilePath;
public ConfigurationSource(String configFilePath)
{
this.configFilePath = configFilePath;
logger = new Logger(getClass());
}
public void initConfig()
@@ -32,52 +31,40 @@ public class ConfigurationSource
* we used to have a config.yml file in the "resources" folder, but that is no longer necessary.
*/
LinkedHashMap<String, Object> internalConfigContents = new LinkedHashMap<>(); // map holding all file entries
for (ConfigurationEntry entry : ConfigurationEntry.values())
for(ConfigurationEntry entry : ConfigurationEntry.values())
{
internalConfigContents.put(entry.getPath(), entry.getDefaultValue());
}
if (internalConfigContents.isEmpty())
if(internalConfigContents.isEmpty())
{
LOGGER.error("Error reading internal configuration!");
logger.log("Error reading internal configuration!");
HidekoBot.shutdown();
return;
}
// check if config files exists in filesystem
File fsConfigFile = new File(configFilePath);
if (!fsConfigFile.exists())
if(!fsConfigFile.exists())
{
// try to create config file
try
{
if (!fsConfigFile.createNewFile())
{
LOGGER.error("We tried creating a file that already exists!");
HidekoBot.shutdown();
return;
}
} catch (IOException e)
{
LOGGER.error("Error creating configuration file!", e);
try { fsConfigFile.createNewFile(); }
catch (IOException e) {
logger.log("Error creating configuration file!");
logger.log(e.getMessage());
HidekoBot.shutdown();
return;
}
}
// load the YAML file from the filesystem
LoaderOptions options = new LoaderOptions();
Yaml fsConfigYaml = new Yaml(new SafeConstructor(options));
Yaml fsConfigYaml = new Yaml();
LinkedHashMap<String, Object> fsConfigContents = null; // map holding all file entries
try (InputStream fsConfigStream = new FileInputStream(fsConfigFile))
{
fsConfigContents = fsConfigYaml.load(fsConfigStream);
} catch (IOException e)
{
LOGGER.error(e.getMessage());
}
{ fsConfigContents = fsConfigYaml.load(fsConfigStream); }
catch (IOException e) { logger.log(e.getMessage()); }
if (fsConfigContents == null) // if file contents are empty or corrupted...
if(fsConfigContents == null) // if file contents are empty or corrupted...
{
// "clean" them (this effectively forces a config file reset)
fsConfigContents = new LinkedHashMap<>();
@@ -85,10 +72,10 @@ public class ConfigurationSource
// check for missing keys
boolean missingKeys = false;
for (String key : internalConfigContents.keySet())
for(String key : internalConfigContents.keySet())
{
// if key is missing
if (!fsConfigContents.containsKey(key))
if(!fsConfigContents.containsKey(key))
{
// quit and flag it, as we need to complete the file with the missing ones
missingKeys = true;
@@ -97,29 +84,24 @@ public class ConfigurationSource
}
// if keys are missing
if (missingKeys)
if(missingKeys)
{
// create a new mixed map that will take existing values from the non-missing keys
// and fill everything else with the default values
LinkedHashMap<String, Object> filledEntries = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : internalConfigContents.entrySet())
for(String key : internalConfigContents.keySet())
{
String key = entry.getKey();
if (fsConfigContents.containsKey(key))
if(fsConfigContents.containsKey(key))
{
// if the key already exists, copy the original value
filledEntries.put(key, fsConfigContents.get(key));
} else
{
} else {
// else, copy the value from the example config file
filledEntries.put(key, entry.getValue());
filledEntries.put(key, internalConfigContents.get(key));
}
}
try
{
try {
// new writer to actually write the contents to the file
PrintWriter missingKeysWriter = new PrintWriter(fsConfigFile);
@@ -132,17 +114,15 @@ public class ConfigurationSource
// create the yaml object and dump the values to filesystem
Yaml yaml = new Yaml(dumperOptions);
yaml.dump(filledEntries, missingKeysWriter);
} catch (FileNotFoundException e)
{
LOGGER.error(e.getMessage());
} catch (FileNotFoundException e) {
logger.log(e.getMessage());
HidekoBot.shutdown();
return;
}
// finally, dump all entries to cache.
loadConfig(filledEntries);
} else
{
} else {
// if no key is missing, just cache all entries and values from filesystem.
loadConfig(fsConfigContents);
}
@@ -153,7 +133,6 @@ public class ConfigurationSource
{
this.configurationEntries.putAll(configurationEntries);
}
public Object getConfigValue(ConfigurationEntry key)
{
return configurationEntries.get(key.getPath());

View File

@@ -0,0 +1,377 @@
package wtf.beatrice.hidekobot.datasources;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.Logger;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class DatabaseSource
{
private final static String sqliteURL = "jdbc:sqlite:%path%";
private Connection dbConnection = null;
private final String dbPath;
private final Logger logger;
public DatabaseSource(String dbPath)
{
this.dbPath = dbPath;
this.logger = new Logger(getClass());
}
public boolean connect()
{
String url = sqliteURL.replace("%path%", dbPath);
if(!close()) return false;
try {
dbConnection = DriverManager.getConnection(url);
logger.log("Database connection established!");
return true;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public boolean close()
{
if (dbConnection != null)
{
try {
if(!dbConnection.isClosed())
{
dbConnection.close();
}
} catch (SQLException e) {
e.printStackTrace();
return false;
}
dbConnection = null;
}
return true;
}
/*
* DB STRUCTURE
* TABLE 1: pending_disabled_messages
* ----------------------------------------------------------------------------------
* | guild_id | channel_id | message_id | expiry_timestamp |
* ----------------------------------------------------------------------------------
* |39402849302 | 39402849302 | 39402849302 | 2022-11-20 22:45:53:300 |
* ---------------------------------------------------------------------------------
*
*
* TABLE 2: command_runners
* --------------------------------------------------------------------------------------------
* | guild_id | channel_id | message_id | user_id | channel_type |
* --------------------------------------------------------------------------------------------
* | 39402849302 | 39402849302 | 39402849302 | 39402849302 | PRIVATE |
* --------------------------------------------------------------------------------------------
*
*/
//todo: javadocs
public boolean initDb()
{
List<String> newTables = new ArrayList<>();
newTables.add("CREATE TABLE IF NOT EXISTS pending_disabled_messages (" +
"guild_id TEXT NOT NULL, " +
"channel_id TEXT NOT NULL," +
"message_id TEXT NOT NULL," +
"expiry_timestamp TEXT NOT NULL " +
");");
newTables.add("CREATE TABLE IF NOT EXISTS command_runners (" +
"guild_id TEXT NOT NULL, " +
"channel_id TEXT NOT NULL," + // channel the command was run in
"message_id TEXT NOT NULL," + // message id of the bot's response
"user_id TEXT NOT NULL, " + // user who ran the command
"channel_type TEXT NOT NULL" + // channel type (PRIVATE, FORUM, ...)
");");
for(String sql : newTables)
{
try (Statement stmt = dbConnection.createStatement()) {
// execute the statement
stmt.execute(sql);
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
return true;
}
public boolean trackRanCommandReply(Message message, User user)
{
String userId = user.getId();
String guildId;
ChannelType channelType = message.getChannelType();
if(!(channelType.isGuild()))
{
guildId = userId;
} else {
guildId = message.getGuild().getId();
}
String channelId = message.getChannel().getId();
String messageId = message.getId();
String query = "INSERT INTO command_runners " +
"(guild_id, channel_id, message_id, user_id, channel_type) VALUES " +
" (?, ?, ?, ?, ?);";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, guildId);
preparedStatement.setString(2, channelId);
preparedStatement.setString(3, messageId);
preparedStatement.setString(4, userId);
preparedStatement.setString(5, channelType.name());
preparedStatement.executeUpdate();
return true;
} catch (SQLException e)
{
e.printStackTrace();
}
return false;
}
public boolean isUserTrackedFor(String userId, String messageId)
{
String trackedUserId = getTrackedReplyUserId(messageId);
if(trackedUserId == null) return false;
return userId.equals(trackedUserId);
}
public ChannelType getTrackedMessageChannelType(String messageId)
{
String query = "SELECT channel_type " +
"FROM command_runners " +
"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())
{
String channelTypeName = resultSet.getString("channel_type");
return ChannelType.valueOf(channelTypeName);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getTrackedReplyUserId(String messageId)
{
String query = "SELECT user_id " +
"FROM command_runners " +
"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("user_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public boolean queueDisabling(Message message)
{
String messageId = message.getId();
String channelId = message.getChannel().getId();
String guildId;
ChannelType channelType = message.getChannelType();
if(!(channelType.isGuild()))
{
guildId = "PRIVATE";
} else {
guildId = message.getGuild().getId();
}
LocalDateTime expiryTime = LocalDateTime.now().plusSeconds(Cache.getExpiryTimeSeconds());
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Cache.getExpiryTimestampFormat());
String expiryTimeFormatted = dateTimeFormatter.format(expiryTime);
String query = "INSERT INTO pending_disabled_messages " +
"(guild_id, channel_id, message_id, expiry_timestamp) VALUES " +
" (?, ?, ?, ?);";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, guildId);
preparedStatement.setString(2, channelId);
preparedStatement.setString(3, messageId);
preparedStatement.setString(4, expiryTimeFormatted);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e)
{
e.printStackTrace();
}
return false;
}
public List<String> getQueuedExpiringMessages()
{
List<String> messages = new ArrayList<>();
String query = "SELECT message_id " +
"FROM pending_disabled_messages ";
try (Statement statement = dbConnection.createStatement())
{
ResultSet resultSet = statement.executeQuery(query);
if(resultSet.isClosed()) return messages;
while(resultSet.next())
{
messages.add(resultSet.getString("message_id"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return messages;
}
public boolean untrackExpiredMessage(String messageId)
{
String query = "DELETE FROM pending_disabled_messages WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
preparedStatement.execute();
} catch (SQLException e)
{
e.printStackTrace();
return false;
}
query = "DELETE FROM command_runners WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
preparedStatement.execute();
} catch (SQLException e)
{
e.printStackTrace();
return false;
}
return true;
}
public String getQueuedExpiringMessageExpiryDate(String messageId)
{
String query = "SELECT expiry_timestamp " +
"FROM pending_disabled_messages " +
"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("expiry_timestamp");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getQueuedExpiringMessageChannel(String messageId)
{
String query = "SELECT channel_id " +
"FROM pending_disabled_messages " +
"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("channel_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getQueuedExpiringMessageGuild(String messageId)
{
String query = "SELECT guild_id " +
"FROM pending_disabled_messages " +
"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("guild_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}

View File

@@ -1,8 +1,7 @@
package wtf.beatrice.hidekobot.datasources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.Logger;
import java.io.IOException;
import java.io.InputStream;
@@ -13,7 +12,7 @@ public class PropertiesSource
private Properties properties = null;
private final String fileName = "default.properties";
private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesSource.class);
private final Logger logger = new Logger(getClass());
public void load()
{
@@ -25,16 +24,14 @@ public class PropertiesSource
{
properties.load(internalPropertiesStream);
} catch (IOException e)
{
LOGGER.error(e.getMessage());
}
catch (IOException e) {
logger.log(e.getMessage());
HidekoBot.shutdown();
return;
}
}
public String getProperty(String property)
{
return properties == null ? "" : properties.getProperty(property);
}
{ return properties == null ? "" : properties.getProperty(property); }
}

View File

@@ -1,77 +0,0 @@
package wtf.beatrice.hidekobot.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "command_runners")
public class CommandRunner
{
@Id
@Column(name = "message_id", nullable = false)
private String messageId;
@Column(name = "guild_id", nullable = false)
private String guildId;
@Column(name = "channel_id", nullable = false)
private String channelId;
@Column(name = "user_id", nullable = false)
private String userId;
@Column(name = "channel_type", nullable = false)
private String channelType; // store JDA enum name
public String getMessageId()
{
return messageId;
}
public void setMessageId(String messageId)
{
this.messageId = messageId;
}
public String getGuildId()
{
return guildId;
}
public void setGuildId(String guildId)
{
this.guildId = guildId;
}
public String getChannelId()
{
return channelId;
}
public void setChannelId(String channelId)
{
this.channelId = channelId;
}
public String getUserId()
{
return userId;
}
public void setUserId(String userId)
{
this.userId = userId;
}
public String getChannelType()
{
return channelType;
}
public void setChannelType(String channelType)
{
this.channelType = channelType;
}
}

View File

@@ -1,64 +0,0 @@
package wtf.beatrice.hidekobot.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "pending_disabled_messages")
public class PendingDisabledMessage
{
@Id
@Column(name = "message_id", nullable = false)
private String messageId;
@Column(name = "guild_id", nullable = false)
private String guildId;
@Column(name = "channel_id", nullable = false)
private String channelId;
@Column(name = "expiry_timestamp", nullable = false)
private String expiryTimestamp; // keep as String to match your format for now
public String getMessageId()
{
return messageId;
}
public void setMessageId(String messageId)
{
this.messageId = messageId;
}
public String getGuildId()
{
return guildId;
}
public void setGuildId(String guildId)
{
this.guildId = guildId;
}
public String getChannelId()
{
return channelId;
}
public void setChannelId(String channelId)
{
this.channelId = channelId;
}
public String getExpiryTimestamp()
{
return expiryTimestamp;
}
public void setExpiryTimestamp(String expiryTimestamp)
{
this.expiryTimestamp = expiryTimestamp;
}
}

View File

@@ -1,103 +0,0 @@
package wtf.beatrice.hidekobot.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "urban_dictionary")
public class UrbanDictionaryEntry
{
@Id
@Column(name = "message_id", nullable = false)
private String messageId;
@Column(name = "page", nullable = false)
private Integer page;
@Column(name = "meanings", nullable = false, columnDefinition = "TEXT")
private String meanings;
@Column(name = "examples", nullable = false, columnDefinition = "TEXT")
private String examples;
@Column(name = "contributors", nullable = false, columnDefinition = "TEXT")
private String contributors;
@Column(name = "dates", nullable = false, columnDefinition = "TEXT")
private String dates;
@Column(name = "term", nullable = false)
private String term;
public String getMessageId()
{
return messageId;
}
public void setMessageId(String messageId)
{
this.messageId = messageId;
}
public Integer getPage()
{
return page;
}
public void setPage(Integer page)
{
this.page = page;
}
public String getMeanings()
{
return meanings;
}
public void setMeanings(String meanings)
{
this.meanings = meanings;
}
public String getExamples()
{
return examples;
}
public void setExamples(String examples)
{
this.examples = examples;
}
public String getContributors()
{
return contributors;
}
public void setContributors(String contributors)
{
this.contributors = contributors;
}
public String getDates()
{
return dates;
}
public void setDates(String dates)
{
this.dates = dates;
}
public String getTerm()
{
return term;
}
public void setTerm(String term)
{
this.term = term;
}
}

View File

@@ -2,55 +2,23 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
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.services.CommandService;
@Component
public class ButtonInteractionListener extends ListenerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(ButtonInteractionListener.class);
private final CommandService commandService;
private final CoinFlip coinFlip;
public ButtonInteractionListener(@Autowired CommandService commandService,
@Autowired CoinFlip coinFlip)
{
this.commandService = commandService;
this.coinFlip = coinFlip;
}
@Override
public void onButtonInteraction(ButtonInteractionEvent event)
{
switch (event.getComponentId().toLowerCase())
{
switch (event.getComponentId().toLowerCase()) {
// coinflip
case "coinflip_reflip" -> coinFlip.buttonReFlip(event);
// generic dismiss button
case "generic_dismiss" -> commandService.deleteUserLinkedMessage(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);
// error handling
default -> LOGGER.warn("Received unhandled {}", event.getClass().getSimpleName());
case "coinflip_reflip" -> CoinFlip.buttonReFlip(event);
// clearchat command
case "clear_dismiss" -> ClearChat.dismissMessage(event);
}

View File

@@ -7,27 +7,23 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import wtf.beatrice.hidekobot.objects.comparators.MessageCommandAliasesComparator;
import wtf.beatrice.hidekobot.util.Logger;
import java.util.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
@Component
public class MessageCommandListener extends ListenerAdapter
{
// map storing command labels and command object alphabetically.
private final TreeMap<LinkedList<String>, MessageCommand> registeredCommands =
new TreeMap<>(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 static final String COMMAND_REGEX = "(?i)^(hideko|hde)\\b";
private final String commandRegex = "(?i)^(hideko|hde)\\b";
// (?i) -> case insensitive flag
// ^ -> start of string (not in middle of a sentence)
// \b -> the word has to end here
@@ -40,16 +36,12 @@ public class MessageCommandListener extends ListenerAdapter
public MessageCommand getRegisteredCommand(String label)
{
for (Map.Entry<LinkedList<String>, MessageCommand> entry : registeredCommands.entrySet())
for(LinkedList<String> aliases : registeredCommands.keySet())
{
LinkedList<String> aliases = entry.getKey();
for (String currentAlias : aliases)
for(String currentAlias : aliases)
{
if (label.equals(currentAlias))
{
return entry.getValue();
}
if(label.equals(currentAlias))
{ return registeredCommands.get(aliases); }
}
}
@@ -57,25 +49,26 @@ public class MessageCommandListener extends ListenerAdapter
}
public LinkedList<MessageCommand> getRegisteredCommands()
{
return new LinkedList<>(registeredCommands.values());
}
{ return new LinkedList<>(registeredCommands.values()); }
private final Logger logger = new Logger(MessageCommandListener.class);
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
// check if a bot is sending this message, and ignore it
if (event.getAuthor().isBot()) return;
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, ...)
if (!eventMessage.toLowerCase().matches("(?s)" + COMMAND_REGEX + ".*"))
if(!eventMessage.toLowerCase().matches(commandRegex + "((.|\\n)*)"))
return;
// generate args from the string
String argsString = eventMessage.replaceAll(COMMAND_REGEX + "\\s*", "");
String argsString = eventMessage.replaceAll(commandRegex + "\\s*", "");
// if no args were specified apart from the bot prefix
@@ -83,11 +76,9 @@ public class MessageCommandListener extends ListenerAdapter
// and that element is the whole string passed as a single argument, which would be empty in this case
// (or contain text in other cases like "string split ," if the passed text doesn't contain any comma ->
// it will be the whole text as a single element.
if (argsString.isEmpty())
if(argsString.isEmpty())
{
event.getMessage()
.reply("Hello there! ✨ Type `" + Cache.getBotPrefix() + " help` to get started!")
.queue();
event.getMessage().reply("Hello there! ✨").queue();
return;
}
@@ -98,13 +89,9 @@ public class MessageCommandListener extends ListenerAdapter
String commandLabel = argsRaw[0];
MessageCommand commandObject = getRegisteredCommand(commandLabel);
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
*/
return;
}
@@ -113,33 +100,37 @@ public class MessageCommandListener extends ListenerAdapter
// permissions check
List<Permission> requiredPermissions = commandObject.getPermissions();
if (requiredPermissions != null && !requiredPermissions.isEmpty())
if(requiredPermissions != null && !requiredPermissions.isEmpty())
{
if (channelType.isGuild()) //todo: what about forum post
if(channelType.isGuild()) //todo: what about forum post
{
Member member = event.getMember();
GuildChannel channel = event.getGuildChannel(); //todo: what about forum post
if (member != null && !member.hasPermission(channel, requiredPermissions))
if(member != null)
{
event.getMessage()
.reply("You do not have permissions to run this command!")
.queue(); // todo prettier
// todo: queue message deletion in 15 seconds or so
return;
if(!member.hasPermission(channel, requiredPermissions))
{
event.getMessage()
.reply("You do not have permissions to run this command!")
.queue(); // todo prettier
// todo: queue message deletion in 15 seconds or so
return;
}
}
}
}
String[] commandArgs;
if (commandObject.passRawArgs())
if(commandObject.passRawArgs())
{
// remove first argument, which is the command label
argsString = argsString.replaceAll("^[\\S]+\\s*", "");
// pass all other arguments as a single argument as the first array element
commandArgs = new String[]{argsString};
} else
}
else
{
// copy all split arguments to the array, except from the command label
commandArgs = Arrays.copyOfRange(argsRaw, 1, argsRaw.length);

View File

@@ -6,51 +6,49 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.util.Logger;
@Component
public class MessageLogger extends ListenerAdapter
{
// this class only gets loaded as a listener if verbosity is set to true on startup.
private static final String GUILD_MESSAGE_LOG_FORMAT = "[%guild%] [#%channel%] %user%: %message%";
private static final String DIRECT_MESSAGE_LOG_FORMAT = "[DM] %user%: %message%";
private final static String guildChannelFormat = "[%guild%] [#%channel%] %user%: %message%";
private final static String dmFormat = "[DM] %user%: %message%";
private static final Logger LOGGER = LoggerFactory.getLogger(MessageLogger.class);
private final Logger logger = new Logger(MessageLogger.class);
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
String toLog = "";
String userName = event.getAuthor().getName();
String userName = event.getAuthor().getAsTag();
String message = event.getMessage().getContentDisplay();
if (event.getChannel() instanceof TextChannel channel)
if(event.getChannel() instanceof TextChannel)
{
String guildName = channel.getGuild().getName();
String guildName = ((TextChannel) event.getChannel()).getGuild().getName();
String channelName = event.getChannel().getName();
toLog = GUILD_MESSAGE_LOG_FORMAT
toLog = guildChannelFormat
.replace("%guild%", guildName)
.replace("%channel%", channelName);
} else if (event.getChannel() instanceof PrivateChannel)
}
else if(event.getChannel() instanceof PrivateChannel)
{
toLog = DIRECT_MESSAGE_LOG_FORMAT;
toLog = dmFormat;
}
toLog = toLog
.replace("%user%", userName)
.replace("%message%", message);
LOGGER.info(toLog);
logger.log(toLog);
if (!event.getMessage().getAttachments().isEmpty())
if(!event.getMessage().getAttachments().isEmpty())
{
for (Message.Attachment atch : event.getMessage().getAttachments())
for(Message.Attachment atch : event.getMessage().getAttachments())
{
LOGGER.info(atch.getUrl());
logger.log(atch.getUrl());
}
}
}

View File

@@ -1,29 +0,0 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Trivia;
@Component
public class SelectMenuInteractionListener extends ListenerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(SelectMenuInteractionListener.class);
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event)
{
switch (event.getComponentId().toLowerCase())
{
// trivia
case "trivia_categories" -> Trivia.handleMenuSelection(event);
// error handling
default -> LOGGER.warn("Received unhandled {}", event.getClass().getSimpleName());
}
}
}

View File

@@ -2,13 +2,11 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.SlashArgumentsCompleter;
import java.util.LinkedList;
import java.util.TreeMap;
@Component
public class SlashCommandCompletionListener extends ListenerAdapter
{
@@ -23,21 +21,16 @@ public class SlashCommandCompletionListener extends ListenerAdapter
}
public SlashArgumentsCompleter getRegisteredCompleter(String label)
{
return registeredCompleters.get(label);
}
{ return registeredCompleters.get(label); }
public LinkedList<SlashArgumentsCompleter> getRegisteredCompleters()
{
return new LinkedList<>(registeredCompleters.values());
}
{ return new LinkedList<>(registeredCompleters.values()); }
@Override
public void onCommandAutoCompleteInteraction(CommandAutoCompleteInteractionEvent event)
{
String commandName = event.getName().toLowerCase();
SlashArgumentsCompleter completer = registeredCompleters.get(commandName);
if (completer == null) return;
if(completer == null) return;
// not running in a thread because nothing heavy should be done here...
completer.runCompletion(event);

View File

@@ -3,13 +3,11 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.LinkedList;
import java.util.TreeMap;
@Component
public class SlashCommandListener extends ListenerAdapter
{
@@ -23,24 +21,20 @@ public class SlashCommandListener extends ListenerAdapter
}
public SlashCommand getRegisteredCommand(String label)
{
return registeredCommands.get(label);
}
{ return registeredCommands.get(label); }
public LinkedList<SlashCommand> getRegisteredCommands()
{
return new LinkedList<>(registeredCommands.values());
}
{ return new LinkedList<>(registeredCommands.values()); }
@Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event)
{
String commandName = event.getName().toLowerCase();
SlashCommand command = registeredCommands.get(commandName);
if (command == null) return;
if(command == null) return;
// finally run the command, in a new thread to avoid locking the main one.
// finally run the command, in a new thread to avoid locking.
new Thread(() -> command.runSlashCommand(event)).start();
}
}

View File

@@ -1,4 +1,4 @@
package wtf.beatrice.hidekobot.objects.fun;
package wtf.beatrice.hidekobot.objects;
import wtf.beatrice.hidekobot.util.RandomUtil;

View File

@@ -1,44 +0,0 @@
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;
import java.util.Arrays;
import java.util.Objects;
public record MessageResponse(@Nullable String content,
@Nullable MessageEmbed embed,
@Nullable ItemComponent... components)
{
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MessageResponse response = (MessageResponse) o;
return Objects.equals(content, response.content) &&
Objects.equals(embed, response.embed) &&
Arrays.equals(components, response.components);
}
@Override
public int hashCode()
{
int result = Objects.hash(content, embed);
result = 31 * result + Arrays.hashCode(components);
return result;
}
@Override
public String toString()
{
return "MessageResponse{" +
"content=" + content +
", embed=" + embed +
", components=" + Arrays.toString(components) +
'}';
}
}

View File

@@ -1,22 +0,0 @@
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;
}
}

View File

@@ -2,7 +2,6 @@ package wtf.beatrice.hidekobot.objects.commands;
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 java.util.LinkedList;
@@ -40,40 +39,17 @@ public interface MessageCommand
*/
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.
*
* @param event the received message event. It should not be used for parsing message contents data as
* the arguments already account for it in a better way.
*
* @param label the command label that was used, taken from all available command aliases.
* @param args a pre-formatted list of arguments, excluding the bot prefix and the command name.
* This is useful because command logic won't have to change in case the bot prefix is changed,
* removed, or we switch to another method of triggering commands (ping, trigger words, ...).
*
* @param args a pre-formatted list of arguments, excluding the bot prefix and the command name.
* This is useful because command logic won't have to change in case the bot prefix is changed,
* removed, or we switch to another method of triggering commands (ping, trigger words, ...).
*/
void runCommand(MessageReceivedEvent event, String label, String[] args);
}

View File

@@ -12,7 +12,6 @@ public interface SlashArgumentsCompleter
* @return the command object.
*/
SlashCommand getCommand();
/**
* Run the argument-completion logic by parsing the event and replying accordingly.
*

View File

@@ -6,16 +6,13 @@ import org.jetbrains.annotations.NotNull;
public class SlashArgumentsCompleterImpl implements SlashArgumentsCompleter
{
private final SlashCommand parentCommand;
public SlashArgumentsCompleterImpl(SlashCommand parentCommand)
{
this.parentCommand = parentCommand;
}
public SlashCommand getCommand()
{
return parentCommand;
}
{ return parentCommand; }
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event)
{

View File

@@ -21,7 +21,6 @@ public interface SlashCommand
* @return the command data object.
*/
CommandData getSlashCommandData();
/**
* Run the command logic by parsing the event and replying accordingly.
*

View File

@@ -8,20 +8,17 @@ public class SlashCommandImpl implements SlashCommand
{
@Override
public String getCommandName()
{
public String getCommandName() {
return getSlashCommandData().getName();
}
@Override
public CommandData getSlashCommandData()
{
public CommandData getSlashCommandData() {
return null;
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event) {
event.reply("Base command implementation").queue();
}
}

View File

@@ -6,15 +6,13 @@ import java.util.LinkedList;
/**
* This class gets two linked lists, and compares their first value alphabetically.
*/
public class MessageCommandAliasesComparator implements Comparator<LinkedList<String>>
{
public class MessageCommandAliasesComparator implements Comparator<LinkedList<String>> {
@Override
public int compare(LinkedList<String> linkedList, LinkedList<String> t1)
{
public int compare(LinkedList<String> linkedList, LinkedList<String> t1) {
if (linkedList.isEmpty()) return 0;
if (t1.isEmpty()) return 0;
if(linkedList.isEmpty()) return 0;
if(t1.isEmpty()) return 0;
return linkedList.get(0).compareTo(t1.get(0));
}

View File

@@ -1,18 +0,0 @@
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());
}
}

View File

@@ -1,18 +0,0 @@
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
}
}

View File

@@ -1,6 +0,0 @@
package wtf.beatrice.hidekobot.objects.fun;
public record TriviaCategory(String categoryName, int categoryId)
{
}

View File

@@ -1,9 +0,0 @@
package wtf.beatrice.hidekobot.objects.fun;
import java.util.List;
public record TriviaQuestion(String question, String correctAnswer,
List<String> wrongAnswers)
{
}

View File

@@ -1,37 +0,0 @@
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.getName() + "," + score + "]";
}
}

View File

@@ -1,8 +0,0 @@
package wtf.beatrice.hidekobot.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import wtf.beatrice.hidekobot.entities.CommandRunner;
public interface CommandRunnerRepository extends JpaRepository<CommandRunner, String>
{
}

View File

@@ -1,8 +0,0 @@
package wtf.beatrice.hidekobot.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import wtf.beatrice.hidekobot.entities.PendingDisabledMessage;
public interface PendingDisabledMessageRepository extends JpaRepository<PendingDisabledMessage, String>
{
}

View File

@@ -1,8 +0,0 @@
package wtf.beatrice.hidekobot.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import wtf.beatrice.hidekobot.entities.UrbanDictionaryEntry;
public interface UrbanDictionaryRepository extends JpaRepository<UrbanDictionaryEntry, String>
{
}

View File

@@ -1,66 +1,157 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.services.CommandService;
import wtf.beatrice.hidekobot.services.DatabaseService;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.util.Logger;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class ExpiredMessageTask implements Runnable
{
private final DatabaseService databaseService;
private final CommandService commandService;
public class ExpiredMessageTask implements Runnable {
private final DateTimeFormatter formatter;
private static final Logger LOGGER = LoggerFactory.getLogger(ExpiredMessageTask.class);
private final Logger logger;
private DatabaseSource databaseSource;
public ExpiredMessageTask(DatabaseService databaseService,
CommandService commandService)
public ExpiredMessageTask()
{
this.databaseService = databaseService;
this.commandService = commandService;
String format = Cache.getExpiryTimestampFormat();
formatter = DateTimeFormatter.ofPattern(format);
databaseSource = Cache.getDatabaseSource();
logger = new Logger(getClass());
}
@Override
public void run()
{
public void run() {
List<String> expiringMessages = databaseService.getQueuedExpiringMessages();
if (expiringMessages == null || expiringMessages.isEmpty()) return;
databaseSource = Cache.getDatabaseSource();
if(databaseSource == null) return;
List<String> expiringMessages = Cache.getDatabaseSource().getQueuedExpiringMessages();
if(expiringMessages == null || expiringMessages.isEmpty()) return;
LocalDateTime now = LocalDateTime.now();
for (String messageId : expiringMessages)
for(String messageId : expiringMessages)
{
if (Cache.isVerbose()) LOGGER.info("expired check: {}", messageId);
if(Cache.isVerbose()) logger.log("expired check: " + messageId);
String expiryTimestamp = databaseService.getQueuedExpiringMessageExpiryDate(messageId);
if (expiryTimestamp == null || expiryTimestamp.isEmpty()) // if missing timestamp
String expiryTimestamp = databaseSource.getQueuedExpiringMessageExpiryDate(messageId);
if(expiryTimestamp == null || expiryTimestamp.equals("")) // if missing timestamp
{
// count it as already expired
databaseService.untrackExpiredMessage(messageId);
databaseSource.untrackExpiredMessage(messageId);
// move on to next message
continue;
}
LocalDateTime expiryDate = LocalDateTime.parse(expiryTimestamp, formatter);
if (now.isAfter(expiryDate))
if(now.isAfter(expiryDate))
{
if (Cache.isVerbose()) LOGGER.info("expired: {}", messageId);
commandService.disableExpired(messageId);
if(Cache.isVerbose()) logger.log("expired: " + messageId);
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);
});
}
}

View File

@@ -1,8 +1,7 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.Logger;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -10,16 +9,22 @@ import java.net.URL;
public class HeartBeatTask implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(HeartBeatTask.class);
private final Logger logger;
public HeartBeatTask()
{
logger = new Logger(getClass());
}
@Override
public void run()
{
String urlString = Cache.getFullHeartBeatLink();
if (urlString == null || urlString.isEmpty()) return;
if(urlString == null || urlString.isEmpty()) return;
try
{
try {
URL heartbeatUrl = new URL(urlString);
@@ -29,18 +34,18 @@ public class HeartBeatTask implements Runnable
connection.setReadTimeout(5000);
int responseCode = connection.getResponseCode();
if (200 <= responseCode && responseCode < 300)
if(200 <= responseCode && responseCode < 300)
{
// only log ok response codes when verbosity is enabled
if (Cache.isVerbose()) LOGGER.info("Heartbeat response code: {}", responseCode);
} else
if(Cache.isVerbose()) logger.log("Heartbeat response code: " + responseCode);
}
else
{
LOGGER.error("Heartbeat returned problematic response code: {}", responseCode);
logger.log("Heartbeat returned problematic response code: " + responseCode);
}
} catch (IOException e)
{
LOGGER.error("Error while trying to push heartbeat", e);
} catch (IOException e) {
logger.log("Error while trying to push heartbeat: " + e.getMessage());
}
}

View File

@@ -1,30 +0,0 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
/**
* This runnable pulls a random seed from random.org and used it to feed a SecureRandom,
* if the integration is enabled.
* <br/>
* This is necessary since we are not directly accessing random.org for each random number we
* need, and thus, only the seed is random - not the algorithm applied to it to compute the numbers.
*/
public class RandomOrgSeedTask implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(RandomOrgSeedTask.class);
@Override
public void run()
{
if (RandomUtil.isRandomOrgKeyValid())
{
if (Cache.isVerbose()) LOGGER.info("Updating Random seed from random.org...");
RandomUtil.initRandomOrg();
if (Cache.isVerbose()) LOGGER.info("Random.org seed updated!");
}
}
}

View File

@@ -1,31 +0,0 @@
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));
}
}

Some files were not shown because too many files have changed in this diff Show More