Compare commits

...

No commits in common. "main" and "python" have entirely different histories.
main ... python

44 changed files with 140 additions and 2789 deletions

11
.gitignore vendored
View File

@ -1,8 +1,3 @@
.idea
target
/target
/.idea
config.yml
dependency-reduced-pom.xml
out
/out/
__pycache__
config.ini
*.swp

16
LICENSE
View File

@ -1,15 +1,15 @@
ISC License
Copyright (c) 2022, oko
Copyright (c) 2022 oko
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

19
commands/fun.py Normal file
View File

@ -0,0 +1,19 @@
from discord.commands import slash_command
from discord.ext import commands
from datetime import date
class Fun(commands.Cog, name="Fun"):
def __init__(self, bot):
self.bot = bot
@slash_command(name='sdate', description='Returns the current Eternal September date')
async def sdate(self, ctx):
sdate = (date.today() - date(1993, 8, 30)).days
await ctx.respond(f"Today is September {sdate}, 1993.")
@slash_command(name='random', description='Random Functions!')
async def random(self, ctx):
await ctx.respond("4") ## pretty random lol
def setup(bot):
bot.add_cog(Fun(bot))

35
commands/moderation.py Normal file
View File

@ -0,0 +1,35 @@
import discord.commands
from discord.ext import commands
from configmanager import ConfigManager, LoggingManager
import datetime
memberlogs = int(ConfigManager.get()['Channels']['MemberLogs'])
class Moderation(commands.Cog, name='Moderation'):
def __init__(self, bot):
self.bot = bot
@commands.Cog.listener() # Join log
async def on_member_join(self, member):
channel = self.bot.get_channel(memberlogs)
try:
embed = discord.Embed(color=discord.Color.green(), description=f'{member.mention} | **Joined Discord**: {member.created_at.date()}', timestamp=datetime.datetime.now())
embed.set_author(name=f'{member} ({member.id})', icon_url=f'{member.display_avatar.url}?128')
embed.set_footer(text='User Left')
await channel.send(embeds=[embed])
except:
LoggingManager.log.error(f'Failed to send joinlog for {member} ({member.id}) in {channel}.')
@commands.Cog.listener() # Leave log
async def on_member_remove(self, member):
channel = self.bot.get_channel(memberlogs)
try:
embed = discord.Embed(color=discord.Color.red(), description=f'{member.mention} | **Joined Server**: {member.joined_at.date()}', timestamp=datetime.datetime.now())
embed.set_author(name=f'{member} ({member.id})', icon_url=f'{member.display_avatar.url}?128')
embed.set_footer(text='User Left')
await channel.send(embeds=[embed])
except:
LoggingManager.log.error(f'Failed to send leavelog for {member} ({member.id}) in {channel}.')
def setup(bot):
bot.add_cog(Moderation(bot))

0
commands/utility.py Normal file
View File

15
config.example.ini Normal file
View File

@ -0,0 +1,15 @@
[Bot]
Token =
Status =
[Database]
Database =
User =
Password =
Host =
Port =
[Channels]
MemberLogs =
ModLogs =
Skynet =

17
configmanager.py Normal file
View File

@ -0,0 +1,17 @@
import configparser
import logging
class LoggingManager():
logging.basicConfig(level=logging.INFO)
log = logging.getLogger()
class ConfigManager():
def get():
config = configparser.ConfigParser()
config.read('config.ini')
return config
def set():
config = configparser.ConfigParser()
config.read('config.ini')
return config

18
kiafumi.py Normal file
View File

@ -0,0 +1,18 @@
import discord
from configmanager import ConfigManager, LoggingManager
intents = discord.Intents.default()
intents.members = True
bot = discord.Bot(intents=intents)
token = ConfigManager.get()['Bot']['Token']
class Kiafumi(discord.Bot):
@bot.event
async def on_ready():
LoggingManager.log.info(f'Logged on as {bot.user}!')
for ext in ['commands.fun', 'commands.moderation']:
bot.load_extension(ext)
bot.run(token) # token

18
lib/database.py Normal file
View File

@ -0,0 +1,18 @@
import psycopg2
class Database:
def __init__(self, db, user, password, host, port, init):
if host == 0:
host = "localhost"
if port == 0:
port = 5432
__conn = psycopg.connect(dbname=db, user=user, password=password, host=host, port=port)
cur = __conn.cursor
def write():
pass
def read():
pass

143
pom.xml
View File

@ -1,143 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>moe.oko</groupId>
<artifactId>Kiafumi</artifactId>
<name>Kiafumi</name>
<version>0.9.0</version>
<packaging>jar</packaging>
<url>https://oko.moe/kiafumi.htm</url>
<properties>
<project.jdk.version>17</project.jdk.version>
<mainclass>moe.oko.Kiafumi.Kiafumi</mainclass>
</properties>
<dependencies>
<!-- JDA (https://mvnrepository.com/artifact/net.dv8tion/JDA) -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-alpha.9</version>
<scope>compile</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>me.carleslc.Simple-YAML</groupId>
<artifactId>Simple-Yaml</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>com.sedmelluq</groupId>
<artifactId>lavaplayer</artifactId>
<version>1.3.53</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
<!-- DDG api (https://github.com/nstrydom2/duckduckgo-api) -->
<dependency>
<groupId>com.github.nstrydom2</groupId>
<artifactId>duckduckgo-api</artifactId>
<version>v0.1.2</version>
</dependency>
<!-- Class Loading API for Command Loading -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com/</url>
</repository>
<repository>
<id>central</id>
<name>bintray</name>
<url>https://jcenter.bintray.com</url>
</repository>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${project.jdk.version}</source>
<target>${project.jdk.version}</target>
<compilerVersion>${project.jdk.version}</compilerVersion>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<index>true</index>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${mainclass}</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,35 +1,12 @@
# Kiafumi
A kitchen-sink discord bot designed to replace [Skyra](https://skyra.pw) v7 for use in the [HC](https://discord.gg/utgAEV2) network.
It's written in Java, using [JDA](https://github.com/DV8FromTheWorld/JDA).
It's written in [Pycord](https://pycord.dev) and is intended to be resource conscious where possible.
## Features
+ The expected utility command suite.
+ Music functionality using Lavaplayer.
+ Member Leave/Join logging.
+ Channel/Conversation archival and export.
## Installation
1. Download from the [Releases](https://fem.mint.lgbt/oko/kiafumi/releases), or compile with `maven clean package`.
2. Run `Kiafumi-x.x.jar` in an empty directory.
3. Create a new MariaDB(**Not** MySQL) database.
4. Open and configure the generated `config.yml`.
5. Run `Kiafumi-x.x.jar` again.
## Credits & Thanks
+ [oko](https://oko.moe)
+ [Tiddy](https://tiddy.38.fail)
+ [Laika](http://alpinia.link), for writing the groundwork of the Java fork.
### Credits & Thanks
+ [oko](https://oko.moe) (Development)
+ [Tiddy](https://tiddy.oko.moe) (Development)
+ [Favna](https://favware.tech), for maintaining [Ribbon](https://github.com/favna/ribbon) from 2017-2019, & Skyra to this day!
### Feedback
Feedback from anyone, from bug reports to feature requests, is greatly encouraged. You can contact us with the [Issues](https://fem.mint.lgbt/oko/kiafumi/issues) page, or reach `oko#0994` on discord.
## License
### License
Copyright © 2022 oko, under the [ISC License](./LICENSE)

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
py-cord
psycopg2-binary

View File

@ -1,252 +0,0 @@
package moe.oko.Kiafumi;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.command.CommandRegistrar;
import moe.oko.Kiafumi.listener.MainListener;
import moe.oko.Kiafumi.listener.SkynetListener;
import moe.oko.Kiafumi.model.db.KiafumiDB;
import moe.oko.Kiafumi.model.ServerManager;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
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.entities.Guild;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.ChunkingFilter;
import net.dv8tion.jda.api.utils.MemberCachePolicy;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import org.simpleyaml.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Files;
import java.time.ZonedDateTime;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.*;
/**
* Kiafumi Main Class
* @author Kay, oko, Tiddy
* @version 0.9.0-pre
* @apiNote Thanks to:
* | Maxopoly, Orinnari, ProgrammerDan, and more, for helping teach Kay how to code Java from scratch.
* | Favna, and the HC Development community for encouraging the development core of HC.
* | HC as a whole, for being a wonderful community and encouraging the creation of this bot
* | Civ, for encouraging us all to sort out how the hell programming works
* | Discord, for offering a platform to learn these skills on
* | The Java Development Team, for making a Language I personally love and adore - Kay
*/
public class Kiafumi {
private final File CONFIG_FILE = new File("config.yml");
public YamlConfiguration yamlConfiguration = new YamlConfiguration();
public String footer = EmbedUI.BRAND + " 0.9.0-pre";
public static Kiafumi instance;
public static JDA JDA;
public List<CommandClass> activeCommands;
public KiafumiConfig config;
public KiafumiDB database;
public ServerManager serverManager;
/**
* Main Class function.
* @param args - Arguments for program start.
*/
public static void main(String[] args) {
try {
var kia = new Kiafumi();
kia.start();
} catch (Exception ex) {
System.out.println("Failed to start Kiafumi, check your Java installation.");
ex.printStackTrace();
}
}
/**
* Ran on program start. Anything in here can determine whether the program will start.
*/
public void start() {
instance = this;
info("Starting Kiafumi.");
// All commands to be loaded on startup!
activeCommands = new CommandRegistrar().getCommandClasses();
// Ensuring the configuration file is generated and/or exists.
if (!CONFIG_FILE.exists()) {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("config.yml")) {
// Save the default config
Files.copy(is, CONFIG_FILE.toPath());
} catch (Exception ex) {
warn("Failed to create the configuration file. Stopping. (" + CONFIG_FILE.getAbsolutePath() + ")");
ex.printStackTrace();
return;
}
}
// Try to load configuration into the configuration API.
try {
yamlConfiguration.load("config.yml");
} catch (FileNotFoundException e) {
e.printStackTrace();
warn("File not found, must've failed to create...");
} catch (Exception e) {
warn("Ensure all values are inputted properly.");
e.printStackTrace();
}
// Initializes our configuration helper & ensures it loads properly.
config = new KiafumiConfig(yamlConfiguration);
if(config.load()) {
info("Fetched Kiafumi config.");
} else {
error("Failed to load configuration. Stopping process.");
Runtime.getRuntime().exit(0);
}
// Registers the stop() function if the program is stopped.
info("Registering shutdown hook.");
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
// Initializes database and loads credentials.
database = config.createDb();
serverManager = new ServerManager();
// Makes our JDA instance.
startDiscord();
}
/**
* Ran on program shutdown.
*/
public void stop() {
var build = new EmbedBuilder()
.setColor(EmbedUI.FAILURE)
.setTitle("Offline")
.setFooter(footer)
.setTimestamp(ZonedDateTime.now());
JDA.getTextChannelById(config.getLogChannel()).sendMessageEmbeds(build.build()).queue();
info("Shutting down Kiafumi.");
if(database.saveServerInformation()) {
info("Successfully saved server information. Shutting down peacefully.");
} else {
for(int i = 0; i < 15; i++) {
error("FAILED TO SAVE SERVER INFORMATION.");
}
}
}
/**
* Starts a JDA Instance.
*/
public void startDiscord() {
try {
JDA = JDABuilder.create(config.getToken(), GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.DIRECT_MESSAGES, GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_VOICE_STATES)
.setChunkingFilter(ChunkingFilter.ALL)
.setMemberCachePolicy(MemberCachePolicy.ALL)
.disableCache(CacheFlag.EMOTE)
.setActivity(Activity.of(Activity.ActivityType.valueOf(config.getActivityType()), config.getActivityMsg()))
.setStatus(OnlineStatus.valueOf(config.getStatusType()))
.addEventListeners(
new MainListener(),
new SkynetListener()
).build().awaitReady();
} catch(Exception ex) {
error("Initialization broke...");
ex.printStackTrace();
return;
}
registerAllCommands();
info("Finished registering commands.");
var eb = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setTitle("Online")
.setFooter(footer)
.setTimestamp(ZonedDateTime.now());
JDA.getTextChannelById(config.getLogChannel()).sendMessageEmbeds(eb.build()).queue();
}
/**
* Quick method to register commands in all servers.
*/
private void registerAllCommands() {
// Registers slash commands.
info("Registering commands for guilds.");
int i = 0;
for(Guild guild : JDA.getGuilds()) {
registerForGuild(guild);
i++;
}
info("Registered commands for " + i + " guilds.");
// Registers the event listeners for those commands.
for(CommandClass cmd : activeCommands) {
JDA.addEventListener(cmd);
}
}
/**
* Registers all bot commands with the guild provided
* @param guild - guild to have commands provided to
*/
public void registerForGuild(Guild guild) {
debug("Registering commands for guild [" + guild.getName() + ":" + guild.getId() + "]");
int i = 0;
int j = 0;
for(CommandClass cmd : activeCommands) {
for(CommandInfo ci : cmd.getSlashCommandInfo()) {
var cca = guild.upsertCommand(ci.getName(), ci.getDescription());
i++;
if(ci.hasSubCommands()) {
for (String name : ci.getSubCommands().keySet()) {
var si = ci.getSubCommands().get(name);
var sd = new SubcommandData(si.getName(), si.getDescription());
for (String option : si.getOptions().keySet()) {
sd.addOption(si.getOptions().get(option), option, si.getOptionDescriptions().get(option), si.getOptionRequirements().get(option));
}
cca.addSubcommands(sd);
}
}
if(ci.hasOptions()) {
for(String name : ci.getOptions().keySet()) {
// Any intelligent IDE will rage about the option not being used, it's added to the action then executed later, don't edit without reason.
cca.addOption(ci.getOptions().get(name), name, ci.getOptionDescriptions().get(name), ci.getOptionRequirements().get(name));
}
}
// Push w/ modifications.
//trace("Command: " + ci.getName() + " registration on " + guild.getId() + " completed.");
try {
cca.queue();
} catch (Exception ex) {
// Only time this should occur is when a server does not have the proper scope.
error("Failed to queue command for RestAction.");
}
}
}
debug("Registered " + i + " commands for guild [" + guild.getId() + "]");
}
// Gets the active database.
public KiafumiDB getDatabase() { return database; }
// Gets active ServerManager
public ServerManager getServerManager() { return serverManager; }
}

View File

@ -1,140 +0,0 @@
package moe.oko.Kiafumi;
import moe.oko.Kiafumi.model.db.DBCredentials;
import moe.oko.Kiafumi.model.db.KiafumiDB;
import org.simpleyaml.configuration.file.YamlConfiguration;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.error;
import static moe.oko.Kiafumi.util.LoggingManager.info;
/**
* KiafumiConfig class
* Helps out with loading things from config and fetching them in an easy manner.
*/
public class KiafumiConfig {
// Setup
public YamlConfiguration configuration;
/*
Discord Variable Section
*/
private String token;
private String logChannel;
private String ownerId;
private String mainGuild;
private String clientId;
private int defaultInvitePermissionLevel;
/*
Kia Variable Section
*/
private String activityType;
private String activityMsg;
private String statusType;
private List<String> pingResponses;
/*
SQL Variable Section
*/
private String host;
private int port;
private String username;
private String password;
private String database;
private int poolSize;
private long connTimeout;
private long idleTimeout;
private long maxLifetime;
/**
* Constructor for the config.
* @param configuration - the configuration file to be passed through in our friendly YamlConfiguration API :)
*/
public KiafumiConfig(YamlConfiguration configuration) {
//Load config on class creation...
this.configuration = configuration;
}
/**
* Loads all configuration values from config.yml
* @return - whether the values were loaded successfully.
* Note: this function will perpetually be a mess, there is definitely a better way to do this,
* but I refuse to do it a better way because it is EASY this way.
*/
public boolean load() {
try {
// Discord loaders
var discord = configuration.getConfigurationSection("discord");
token = discord.getString("token"); // This used to log the token into stdout ???
logChannel = discord.getString("logChannel");
ownerId = discord.getString("ownerId");
mainGuild = discord.getString("mainGuild");
clientId = discord.getString("clientId");
defaultInvitePermissionLevel = discord.getInt("invitePermissionLevel");
// Log discord settings in a neat table.
info("""
Printing loaded discord configuration.
DISCORD CONFIGURATION
--------------------------------
Log Channel: %s
Owner ID: %s
Primary Guild: %s
Invite URL: %s
--------------------------------""".formatted(logChannel, ownerId, mainGuild, assembleDefaultInvite()));
// Kiafumi loaders
var main = configuration.getConfigurationSection("main");
activityType = main.getString("activityType");
activityMsg = main.getString("activityMsg");
statusType = main.getString("statusType");
pingResponses = main.getStringList("pingResponses");
// SQL loaders
var sql = configuration.getConfigurationSection("sql");
host = sql.getString("host");
port = sql.getInt("port");
username = sql.getString("username");
password = sql.getString("password");
database = sql.getString("database");
poolSize = sql.getInt("poolSize");
connTimeout = sql.getLong("connectionTimeout");
idleTimeout = sql.getLong("idleTimeout");
maxLifetime = sql.getLong("maxLifetime");
} catch(Exception ex) {
ex.printStackTrace();
error("Failed to load configuration!");
return false;
}
return true;
}
/**
* Assembles the default invite that can be passed to a user requesting it.
* @return - the invite
*/
public String assembleDefaultInvite() {
return "https://discord.com/oauth2/authorize?client_id=" + clientId + "&scope=bot+applications.commands&permissions=" + defaultInvitePermissionLevel;
}
public String getToken() { return token; }
public String getActivityType() { return activityType; }
public String getActivityMsg() { return activityMsg; }
public String getStatusType() { return statusType; }
public String getLogChannel() { return logChannel; }
public List<String> getPingResponses() { return pingResponses; }
public DBCredentials getCredentials () { return new DBCredentials(
username, password, host, port, database, poolSize, connTimeout, idleTimeout, maxLifetime);
}
public KiafumiDB createDb() { return new KiafumiDB(getCredentials()); }
}

View File

@ -1,43 +0,0 @@
package moe.oko.Kiafumi.command;
import moe.oko.Kiafumi.util.CommandInfo;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.List;
/**
* CommandClass Abstract Class
* Use this for any commands you make, and ensure they go under the .command class.
*/
public abstract class CommandClass extends ListenerAdapter {
//Is the command enabled?
public abstract boolean isEnabled();
//Whats the name of the Command Package (Specify if multiple, like "Utility" or "Moderation")
public abstract String getName();
/**
* For new slash commands
* @param name - name of the command executed
* @param e - SlashCommandInteractionEvent, used for all references.
*/
public abstract void newCommand(String name, SlashCommandInteractionEvent e);
/**
* Overriding our SlashCommandInteractionEvent that way it can execute a cmd.
* @param event
*/
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
newCommand(event.getName(), event);
}
/**
* Also for the sake of organization. To upsert slash commands.
* Follow as name, description. (for upsertCommand(name, description);
* @return - The name and description for the commands contained within the class.
*/
public abstract List<CommandInfo> getSlashCommandInfo();
}

View File

@ -1,76 +0,0 @@
package moe.oko.Kiafumi.command;
import com.google.common.reflect.ClassPath;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static moe.oko.Kiafumi.util.LoggingManager.error;
import static moe.oko.Kiafumi.util.LoggingManager.info;
import static moe.oko.Kiafumi.util.LoggingManager.debug;
/**
* CommandRegistrar Class
* Used for easy command package loading, we use this to avoid having 20 lines of list.add() methods.
* @author Kay
*/
public class CommandRegistrar {
/**
* Locates all classes that contain the package name provided. Use CAREFULLY.
* @param packageName - the name of the package to look for
* @return - A set of classes that contain that package name.
*/
public Set<Class> findAllClassesContaining(String packageName) {
try {
return ClassPath.from(ClassLoader.getSystemClassLoader())
.getAllClasses()
.stream()
.filter(clazz -> clazz.getPackageName()
.contains(packageName))
.map(clazz -> clazz.load())
.collect(Collectors.toSet());
} catch (Exception ex) {
error("Failed to load classes containing " + packageName + ", check stack.");
ex.printStackTrace();
return null;
}
}
/**
* Utilizes findAllClassesContaining() to find all command classes and return them in a simple manner.
* @return - The CommandClass's located.
*/
public List<CommandClass> getCommandClasses() {
try {
// TODO have this check the classpath that we're under and have it scan *that* instead of hard-coding it to only be moe.oko.Kiafumi path.
var classes = findAllClassesContaining("moe.oko.Kiafumi.command");
List<CommandClass> commands = new ArrayList<>();
debug("Discovered " + classes.size() + " classes containing moe.oko.Kiafumi.command in package class.");
for (Class clazz : classes) {
for (Constructor cnstr : clazz.getConstructors()) {
try {
var obj = cnstr.newInstance(); // making an attempt.
if (obj instanceof CommandClass) {
debug("Loading command class %green(" + cnstr.getName() + ").");
commands.add((CommandClass) obj);
}
} catch (InstantiationException ex) {
// Ignore, this is just us trying to load the CommandClass abstract class.
}
}
}
info("Loaded [" + commands.size() + "] command classes.");
return commands;
} catch (IllegalAccessException | InvocationTargetException exception) {
// Now we don't ignore, this is a core issue.
exception.printStackTrace();
error("Failure in command class loading.");
return null;
}
}
}

View File

@ -1,70 +0,0 @@
package moe.oko.Kiafumi.command.fun;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Dreidel Dreidel...
* @author Tiddy
*/
public class DreidelCommand extends CommandClass {
private final List<String> sides;
public DreidelCommand() {
List<String> sides = new ArrayList<>();
sides.add("Nun");
sides.add("Gimmel");
sides.add("Hay");
sides.add("Shin");
this.sides = sides;
}
@Override
public boolean isEnabled() { return true; }
@Override
public String getName() { return "Dreidel"; }
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("dreidel".equals(name)) {
slashLog(e);
e.deferReply().queue();
var eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("Spinning...")
.setDescription("*brrrrrrrrrrrrrr*")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
var rand = new Random();
var result = sides.get(rand.nextInt(sides.size()));
var eb1 = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("You rolled...")
.setDescription(result + "!")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().editOriginalEmbeds(eb1.build()).completeAfter(3, TimeUnit.SECONDS);
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> si = new ArrayList<>();
si.add(new CommandInfo("dreidel", "Spins a dreidel!", CommandType.COMMAND));
return si;
}
}

View File

@ -1,79 +0,0 @@
package moe.oko.Kiafumi.command.fun;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
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 java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Random User Choice Command
* @author Kay
*/
public class FightCommand extends CommandClass {
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getName() {
return "Fight";
}
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("fight".equals(name)) {
slashLog(e);
e.deferReply().queue();
List<User> usersForRng = new ArrayList<>();
List<String> userNames = new ArrayList<>();
for (OptionMapping option : e.getOptions()) {
usersForRng.add(option.getAsUser());
userNames.add(option.getAsUser().getName());
}
// Done, now roll
var eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("Match in progress...")
.setDescription("*POW! KABLAM! SCHNARF!*")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
var rng = new Random();
var pickedUser = usersForRng.get(rng.nextInt(usersForRng.size()));
var eb1 = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("FATALITY!")
.setDescription(pickedUser.getName() + " wins!")
.setThumbnail(pickedUser.getAvatarUrl())
.setFooter(EmbedUI.BRAND) // TODO: Make this show the list of participants
.setTimestamp(ZonedDateTime.now());
e.getHook().editOriginalEmbeds(eb1.build()).completeAfter(3, TimeUnit.SECONDS);
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
var ci = new CommandInfo("fight", "MORTALLL KOMBATTTT", CommandType.COMMAND);
ci.addOption("value1", "first fighter", OptionType.USER, true);
ci.addOption("value2", "second fighter", OptionType.USER, true);
ci.addOption("value3", "third fighter", OptionType.USER, false);
ci.addOption("value4", "fourth fighter", OptionType.USER, false);
cil.add(ci);
return cil;
}
}

View File

@ -1,60 +0,0 @@
package moe.oko.Kiafumi.command.fun;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Fetches the REAL date
* @author oko
*/
public class SeptemberDateCommand extends CommandClass {
@Override
public boolean isEnabled() { return true; }
@Override
public String getName() { return "SeptemberDate"; }
final LocalDate september = LocalDate.of(1993, Month.AUGUST, 31);
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("sdate".equals(name)) {
slashLog(e);
e.deferReply().queue();
var now = LocalDate.now();
// Create the Eternal September date
var sdate = ChronoUnit.DAYS.between(september, now);
var eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("sdate")
.setDescription("Today is September, " + sdate + " 1993, the september that never ends")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
cil.add(new CommandInfo("sdate", "Returns the Eternal September date.", CommandType.COMMAND));
return cil;
}
}

View File

@ -1,56 +0,0 @@
package moe.oko.Kiafumi.command.image;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Helpful Avatar grabber command
* @author oko
*/
public class AvatarCommand extends CommandClass {
@Override
public boolean isEnabled() { return true; }
@Override
public String getName() { return "Avatar"; }
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("avatar".equals(name)) {
e.deferReply().queue();
final var user = e.getOptions().size() == 0
? e.getUser()
: e.getOption("user").getAsUser();
slashLog(e, "for user [" + user.getName() + ":" + user.getId() + "]." );
var eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setAuthor(user.getName() + "#" + user.getDiscriminator())
.setImage(user.getEffectiveAvatarUrl() + "?size=2048")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
var ci = new CommandInfo("avatar", "Returns the avatar of the specified user.", CommandType.COMMAND);
ci.addOption("user", "User to fetch.", OptionType.USER, false);
cil.add(ci);
return cil;
}
}

View File

@ -1,234 +0,0 @@
package moe.oko.Kiafumi.command.music;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.model.audio.AudioInfo;
import moe.oko.Kiafumi.model.audio.AudioPlayerSendHandler;
import moe.oko.Kiafumi.model.audio.TrackManager;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static moe.oko.Kiafumi.util.LoggingManager.info;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Music Command
* Some code may be taken from SHIRO Project (ISC License still applies)
* @author Kay, oko
*/
public class MusicCommand extends CommandClass {
private final AudioPlayerManager audioPlayerManager = new DefaultAudioPlayerManager();
private final Map<String, Map.Entry<AudioPlayer, TrackManager>> players = new HashMap<>();
public MusicCommand() { AudioSourceManagers.registerRemoteSources(audioPlayerManager); }
@Override
public boolean isEnabled() { return true; }
@Override
public String getName() { return "Music"; }
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
switch (name) {
case "play" -> {
e.deferReply().queue();
var input = e.getOption("url").getAsString();
slashLog(e, "with search \"%s\".".formatted(input));
if(input.startsWith("https://"))
loadTrack(input, e.getMember(), e.getHook());
else
loadTrack("ytsearch: " + input, e.getMember(), e.getHook());
}
case "skip" -> {
e.deferReply().queue();
slashLog(e);
if (isAdmin(e.getMember())) {
getPlayer(e.getGuild()).stopTrack();
e.getHook().sendMessage("Skipping the current track.").queue();
}
else {
var info = getTrackManager(e.getGuild()).getTrackInfo(getPlayer(e.getGuild()).getPlayingTrack());
if (info.hasVoted(e.getUser()))
e.getHook().setEphemeral(true).sendMessage("You've already voted to skip this track.");
else {
int votes = info.getSkips();
int users = info.getAuthor().getVoiceState().getChannel().getMembers().size()-1;
int requiredVotes = users/2;
if (votes > requiredVotes) {
getPlayer(e.getGuild()).stopTrack();
e.getHook().sendMessage("Skipping the current track.").queue();
} else {
info.addSkip(e.getUser());
e.getHook().sendMessage("**%s** has voted to skip the track (%s/%s)"
.formatted(e.getUser().getName(), votes, requiredVotes)).queue();
}
}
}
}
case "nowplaying" -> {
e.deferReply().queue();
slashLog(e);
if (!hasPlayer(e.getGuild()) || getPlayer(e.getGuild()).getPlayingTrack() == null)
e.getHook().sendMessage("No song is currently playing.").queue();
else {
var track = getPlayer(e.getGuild()).getPlayingTrack().getInfo();
e.getHook().sendMessageEmbeds(new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setAuthor("Now playing")
.setDescription("[%s](%s)".formatted(track.title, track.uri))
.addField("Info", "Channel: %s\nLength: %s".formatted(track.author, getTimestamp(track.length)), false)
.setFooter("Requested by: " + e.getUser().getName()).build()).queue();
}
}
case "queue" -> {
e.deferReply().queue();
slashLog(e);
if (!hasPlayer(e.getGuild()) || getTrackManager(e.getGuild()).getQueuedTracks().isEmpty())
e.getHook().sendMessage("There is nothing queued.").queue();
else {
var trackList = new StringBuilder();
var queuedTracks = getTrackManager(e.getGuild()).getQueuedTracks();
final short[] trackSize = {-1};
queuedTracks.forEach(audioInfo -> {
trackList.append(buildQueueString(audioInfo));
trackSize[0]++;
});
var eb = new EmbedBuilder().setColor(EmbedUI.SUCCESS);
if (trackList.length() >= 990) {
eb.addField("Queue", "**>** " + trackList.toString(), false);
} else {
eb.addField("Queue", "**>** " + getPlayer(e.getGuild()).getPlayingTrack().getInfo().title
+ "\n" + trackSize[0] + " other tracks..", false).build();
}
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
}
}
private void loadTrack(String input, Member author, InteractionHook hook) {
if (author.getVoiceState().getChannel() == null) {
hook.setEphemeral(true).sendMessage("You are not in a voice channel.");
return;
}
var server = author.getGuild();
getPlayer(server);
audioPlayerManager.loadItemOrdered(server, input, new AudioLoadResultHandler() {
EmbedBuilder eb;
@Override
public void trackLoaded(AudioTrack audioTrack) {
var trackInfo = audioTrack.getInfo();
eb = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setAuthor("Playing")
.setDescription("[%s](%s)".formatted(trackInfo.title, trackInfo.uri))
.addField("Info", "Channel: %s\nLength: %s".formatted(trackInfo.author, getTimestamp(trackInfo.length)), false)
.setFooter("Requested by: " + author.getEffectiveName());
hook.sendMessageEmbeds(eb.build()).queue();
getTrackManager(server).queue(audioTrack, author);
}
@Override
public void playlistLoaded(AudioPlaylist audioPlaylist) {
if (input.startsWith("ytsearch:")) {
getTrackManager(server).queue(audioPlaylist.getTracks().get(0), author);
var trackInfo = audioPlaylist.getTracks().get(0).getInfo();
eb = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setAuthor("Playing")
.setDescription("[%s](%s)".formatted(trackInfo.title, trackInfo.uri))
.addField("Info", "Channel: %s\nLength: %s".formatted(trackInfo.author, getTimestamp(trackInfo.length)), false)
.setFooter("Requested by: " + author.getEffectiveName());
} else {
for (AudioTrack audioTrack : audioPlaylist.getTracks()) {
getTrackManager(server).queue(audioTrack, author);
eb = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setAuthor("Loaded tracks")
.setDescription("**%s** tracks added to the queue.".formatted(audioPlaylist.getTracks().size()))
.setFooter("Requested by: " + author.getEffectiveName());
}
}
hook.sendMessageEmbeds(eb.build()).queue();
}
@Override
public void noMatches() {
eb = new EmbedBuilder()
.setColor(EmbedUI.FAILURE)
.setAuthor("Error")
.setDescription("No matches were found.");
hook.sendMessageEmbeds(eb.build()).queue();
}
@Override
public void loadFailed(FriendlyException e) { hook.sendMessage("Unable to play track.").queue(); }
});
}
private TrackManager getTrackManager(Guild server) { return players.get(server.getId()).getValue(); }
private AudioPlayer getPlayer(Guild server) {
var player = hasPlayer(server) ? players.get(server.getId()).getKey() : createPlayer(server);
return player;
}
private AudioPlayer createPlayer(Guild server) {
var newPlayer = audioPlayerManager.createPlayer();
var manager = new TrackManager(newPlayer);
newPlayer.addListener(manager);
server.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(newPlayer));
players.put(server.getId(), new AbstractMap.SimpleEntry<>(newPlayer, manager));
return newPlayer;
}
private boolean hasPlayer(Guild server) { return players.containsKey(server.getId()); }
private boolean isAdmin(Member member) { return member.hasPermission(Permission.ADMINISTRATOR); }
private String getTimestamp(long ms) {
return String.format("%02d:%02d:%02d", ms/(3600*1000),
ms/(60*1000) % 60,
ms/1000 % 60);
}
private String buildQueueString(AudioInfo info) {
var trackInfo = info.getTrack().getInfo();
return "%s [%s]\n".formatted(trackInfo.title, getTimestamp(trackInfo.length));
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
var playCommandList = new CommandInfo("play", "Adds a new track to the queue.", CommandType.COMMAND);
playCommandList.addOption("url", "The URL or title of the track you want to play.", OptionType.STRING, true);
cil.add(playCommandList);
cil.add(new CommandInfo("skip", "Votes to skip the current track.", CommandType.COMMAND));
cil.add(new CommandInfo("nowplaying", "Displays the currently played track." , CommandType.COMMAND));
cil.add(new CommandInfo("queue", "Displays the tracks currently queued.", CommandType.COMMAND));
return cil;
}
}

View File

@ -1,74 +0,0 @@
package moe.oko.Kiafumi.command.utility;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import org.bitnick.net.duckduckgo.WebSearch;
import org.bitnick.net.duckduckgo.entity.SearchResult;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Helpful Search Command (Uses DDG API)
* @author Kay
*/
public class DuckCommand extends CommandClass {
private boolean enabled = true;
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public String getName() {
return "Duck";
}
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("search".equals(name)) {
e.deferReply().queue();
String option = e.getOption("query").getAsString();
slashLog(e, "with search \"" + option + "\".");
WebSearch ws = WebSearch.instanceOf();
SearchResult sr;
try {
sr = ws.instantAnswerSearch(option);
} catch (Exception ex) {
EmbedBuilder eb = new EmbedBuilder()
.setColor(EmbedUI.FAILURE)
.setTitle("Query Failed")
.setDescription("Couldn't find any results.")
.setFooter("Query: " + option)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
return;
}
EmbedBuilder eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle(sr.getTitle(), sr.getUrl().toString())
.setDescription(sr.getDescription())
.setFooter("Query: " + option)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
CommandInfo ci = new CommandInfo("search", "Looks up with DuckDuckGo your query!", CommandType.COMMAND);
ci.addOption("query", "The query to be searched", OptionType.STRING, true);
cil.add(ci);
return cil;
}
}

View File

@ -1,87 +0,0 @@
package moe.oko.Kiafumi.command.utility;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
public class HelpCommand extends CommandClass {
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getName() {
return "Help";
}
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("help".equalsIgnoreCase(name)) {
slashLog(e);
e.deferReply().queue();
// We assemble a help string, this is our description for our embed.
var sb = new StringBuilder();
for(CommandClass cc : Kiafumi.instance.activeCommands) {
for(CommandInfo ci : cc.getSlashCommandInfo()) {
var cname = ci.getName();
var cdesc= ci.getDescription();
sb.append("**").append(cname).append("** - __").append(cdesc).append("__\n");
if(ci.hasOptions()) {
for (String opt : ci.getOptions().keySet()) {
sb.append("-> **").append(opt).append("** `").append(ci.getOptions().get(opt).name())
.append("` *").append(ci.getOptionDescriptions().get(opt)).append("*");
if (ci.getOptionRequirements().get(opt)) {
sb.append(" __***[REQUIRED]***__");
}
sb.append("\n");
}
}
if(ci.hasSubCommands()) {
for (String subc : ci.getSubCommands().keySet()) {
var subCommand = ci.getSubCommands().get(subc);
sb.append("-> **").append(subc).append("** *").append(subCommand.getDescription()).append("*\n");
if (subCommand.hasOptions()) {
for (String opt : subCommand.getOptions().keySet()) {
sb.append("--> **").append(opt).append("** `").append(subCommand.getOptions().get(opt).name())
.append("` *").append(subCommand.getOptionDescriptions().get(opt)).append("*");
if (subCommand.getOptionRequirements().get(opt)) {
sb.append(" __***[REQUIRED]***__");
}
sb.append("\n");
}
}
sb.append("\n");
}
}
sb.append("\n");
}
}
EmbedBuilder eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTimestamp(ZonedDateTime.now())
.setFooter(EmbedUI.BRAND)
.setTitle("Help Command")
.setDescription(sb.toString());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
var ci = new CommandInfo("help", "Displays all enabled commands this bot has.", CommandType.COMMAND);
cil.add(ci);
return cil;
}
}

View File

@ -1,90 +0,0 @@
package moe.oko.Kiafumi.command.utility;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Helpful User Information Command
* @author oko
*/
public class InfoCommand extends CommandClass {
@Override
public boolean isEnabled() { return true; }
@Override
public String getName() { return "Info"; }
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
switch (name){
case "userinfo":
e.deferReply().queue();
// Setup variables
final var member = e.getOptions().size() == 0
? e.getMember()
: e.getOption("user").getAsMember();
final var dTF = DateTimeFormatter.ofPattern("MM-dd-yyyy");
slashLog(e, "for user [" + member.getUser().getName() + ":" + member.getId() + "].");
// Build embed
EmbedBuilder eb1 = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setAuthor(member.getEffectiveName() + "#" + member.getUser().getDiscriminator())
.addField("Joined Server", member.getTimeJoined().format(dTF), true)
.addField("Joined Discord", member.getTimeCreated().format(dTF), true)
.addField("Roles", member.getRoles().stream().map(Role::getName).reduce((a, b) -> a + ", " + b).orElse("None"), false)
.setThumbnail(member.getEffectiveAvatarUrl())
.setFooter("ID: " + member.getId())
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb1.build()).queue();
return;
case "serverinfo":
e.deferReply().queue();
// Setup variables
final var guild = e.getGuild();
final var dTF2 = DateTimeFormatter.ofPattern("MM-dd-yyyy");
slashLog(e, "for guild [" + guild.getId() + ":" + guild.getName() + "].");
// Build Embed
EmbedBuilder eb2 = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setAuthor(guild.getName())
.addField("Members", "**" + guild.getMemberCount() + "** member(s) \n Owner: "
+ guild.getOwner().getEffectiveName() + "#" + guild.getOwner().getUser().getDiscriminator(), true)
.addField("Channels", "**" + guild.getTextChannels().size() + "** Text \n **"
+ guild.getVoiceChannels().size() + "** Voice", true)
.addField("Other", "Roles: **" + guild.getRoles().size() + "** \n"
+ "Created: " + guild.getTimeCreated().format(dTF2), true)
.setThumbnail(guild.getIconUrl())
.setFooter("ID: " + guild.getId());
e.getHook().sendMessageEmbeds(eb2.build()).queue();
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
CommandInfo ci = new CommandInfo("userinfo", "Returns information about a user.", CommandType.COMMAND);
ci.addOption("user", "The user to get information about.", OptionType.USER, false);
CommandInfo ci2 = new CommandInfo("serverinfo", "Returns information about the server.", CommandType.COMMAND);
cil.add(ci);
cil.add(ci2);
return cil;
}
}

View File

@ -1,57 +0,0 @@
package moe.oko.Kiafumi.command.utility;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Helpful Invite Command
* @author Kay, oko
*/
public class InviteCommand extends CommandClass {
@Override
public boolean isEnabled() {
return true; //Always enabled
}
@Override
public String getName() {
return "Invite";
}
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if(e.getGuild() == null) { return; }
if ("invite".equals(name)) {
slashLog(e);
e.deferReply().queue();
EmbedBuilder eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setAuthor("Kiafumi", "https://oko.moe/kiafumi.htm", Kiafumi.JDA.getSelfUser().getAvatarUrl())
.setTitle("Invite me to your server!", Kiafumi.instance.config.assembleDefaultInvite())
.setDescription("Alternatively, you can host me yourself!")
.appendDescription(" https://fem.mint.lgbt/oko/kiafumi")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> cil = new ArrayList<>();
CommandInfo ci = new CommandInfo("invite", "Returns an invite for Kiafumi.", CommandType.COMMAND);
cil.add(ci);
return cil;
}
}

View File

@ -1,66 +0,0 @@
package moe.oko.Kiafumi.command.utility;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
/**
* Helpful Ping Command
* @author Kay
*/
public class PingCommand extends CommandClass {
//Always true, ping cmd is EXISTENTIAL
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getName() {
return "Ping";
}
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if ("ping".equals(name.toLowerCase(Locale.ROOT))) {
slashLog(e);
e.deferReply().queue();
long sentMs = e.getTimeCreated().toInstant().toEpochMilli();
long recMs = System.currentTimeMillis();
long ping = recMs - sentMs;
EmbedBuilder eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle(getComedy())
.setDescription("Pong! " + ping + "ms")
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
}
private String getComedy() {
Random r = new Random();
return Kiafumi.instance.config.getPingResponses().get(r.nextInt(Kiafumi.instance.config.getPingResponses().size()));
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> si = new ArrayList<>();
CommandInfo ci = new CommandInfo("ping", "Returns bot latency with a twist!", CommandType.COMMAND);
si.add(ci);
return si;
}
}

View File

@ -1,136 +0,0 @@
package moe.oko.Kiafumi.command.utility;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.command.CommandClass;
import moe.oko.Kiafumi.model.Server;
import moe.oko.Kiafumi.util.CommandInfo;
import moe.oko.Kiafumi.util.CommandType;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.slashLog;
import static moe.oko.Kiafumi.util.LoggingManager.slashResponse;
/**
* Permits modification of server settings, critical class to functionality.
* @author Kay
*/
public class SettingCommand extends CommandClass {
@Override
public boolean isEnabled() {
return true; // Another non-disable command
}
@Override
public String getName() {
return "Settings";
}
@Override
public void newCommand(String name, SlashCommandInteractionEvent e) {
if(e.getGuild() != null) {
Server server = Kiafumi.instance.getServerManager().getOrCreateServer(e.getGuild());
switch (name) {
case "settings" -> {
slashLog(e);
e.deferReply().queue();
// No options, just fire an embed off...
EmbedBuilder eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("Kiafumi Settings")
.setDescription(server.getOpts())
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb.build()).queue();
}
case "setting" -> {
// User is attempting a settings modification. Check if admin.
if(!e.getMember().hasPermission(Permission.ADMINISTRATOR) && !e.getMember().isOwner()) {
slashLog(e, EmbedUI.RESPONSE_PRIVILEGES);
e.deferReply(true).queue();
// Private reply, other people can't see this if ephemeral.
e.getHook().sendMessage("**You cannot run this command.**").queue();
return;
}
switch (e.getSubcommandName().toLowerCase()) {
case "view" -> {
e.deferReply().queue();
String opt = e.getOption("name").getAsString();
slashLog(e, "with subcommand \"" + e.getSubcommandName().toLowerCase() + "\" for setting \"" + opt + "\".");
EmbedBuilder eb1 = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle(opt)
.setDescription("Value: `" + server.getOptionByString(opt) + '`')
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb1.build()).queue();
}
case "set" -> {
e.deferReply().queue();
String opt1 = e.getOption("name").getAsString();
String opt2 = e.getOption("value").getAsString();
slashLog(e, "with subcommand \"" + e.getSubcommandName().toLowerCase() + "\" for setting \"" + opt1 + "\" to \"" + opt2 + "\".");
String response = server.setOptionByString(opt1, opt2);
slashResponse(e, response);
EmbedBuilder eb2 = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setTitle(opt1)
.setDescription(response)
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb2.build()).queue();
}
case "clear" -> {
e.deferReply().queue();
String opt3 = e.getOption("name").getAsString();
slashLog(e, "with subcommand \"" + e.getSubcommandName().toLowerCase() + "\" for setting \"" + opt3 + "\".");
String response1 = server.resetOptionByString(opt3);
slashResponse(e, response1);
EmbedBuilder eb3 = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setTitle(opt3)
.setDescription(response1)
.setFooter(EmbedUI.BRAND)
.setTimestamp(ZonedDateTime.now());
e.getHook().sendMessageEmbeds(eb3.build()).queue();
}
}
}
}
}
}
@Override
public List<CommandInfo> getSlashCommandInfo() {
List<CommandInfo> si = new ArrayList<>();
CommandInfo ci2 = new CommandInfo("setting", "Permits modification, viewing, and clearing of settings.", CommandType.COMMAND);
// For those looking here for inspiration, you CANNOT mix options and subcommands. You can only have one or the other.
CommandInfo ci = new CommandInfo("view", "Shows the current value for the setting provided.", CommandType.SUBCOMMAND);
ci.addOption("name", "The name of the setting to display", OptionType.STRING, true);
ci2.addSubcommand(ci);
CommandInfo ci3 = new CommandInfo("set", "sets a setting for the guild you are in", CommandType.SUBCOMMAND);
ci3.addOption("name", "The name of the setting to modify", OptionType.STRING, true);
ci3.addOption("value", "The value to set the setting to", OptionType.STRING, true);
ci2.addSubcommand(ci3);
CommandInfo ci4 = new CommandInfo("clear", "reverts a setting back to its default value", CommandType.SUBCOMMAND);
ci4.addOption("name", "Name of the setting to clear", OptionType.STRING, true);
ci2.addSubcommand(ci4);
CommandInfo ci5 = new CommandInfo("settings", "displays all settings available for the guild.", CommandType.COMMAND);
si.add(ci5);
si.add(ci2);
return si;
}
}

View File

@ -1,4 +0,0 @@
package moe.oko.Kiafumi.command.utility;
public class StatsCommand {
}

View File

@ -1,64 +0,0 @@
package moe.oko.Kiafumi.listener;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.ReadyEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import java.time.ZonedDateTime;
import static moe.oko.Kiafumi.util.LoggingManager.info;
/**
* Main Listener
* Used for all essential utility listeners, such as guild handling and persistence.
* @author Kay, oko, Tiddy
*/
public class MainListener extends ListenerAdapter {
/**
* GuildJoin event listener, that ensures that a discord has a profile created for it.
* @param event - event to be handled...
*/
@Override
public void onGuildJoin(@NotNull GuildJoinEvent event) {
// Automatically create our default information for the server if we don't have it already.
info("Joined a new guild, NAME: " + event.getGuild().getName() + " ID: " + event.getGuild().getId());
Kiafumi.instance.getServerManager().createNewDefaultServer(event.getGuild());
Kiafumi.instance.registerForGuild(event.getGuild());
}
/**
* Shoots a message into console when the bot is defined as "Ready" by Discord.
*/
@Override
public void onReady(@NotNull ReadyEvent event) {
info("""
Received READY signal from Discord, bot is now logged in.
--------------------------------
Active Guilds: [%s]
Guilds Unavailable: [%s]
--------------------------------""".formatted(event.getGuildAvailableCount(), event.getGuildUnavailableCount()));
}
/**
* Quick Response for if someone pings me.
*/
public void onMessageReceived(MessageReceivedEvent event) {
if(event.getMessage().getMentionedUsers().contains(Kiafumi.JDA.getSelfUser())) {
info("Sent about message in " + event.getGuild().getId());
var eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setTitle("Hi, i'm Kiafumi!")
.setDescription("I was summoned on October 6th 2017! My goal is to explore the metaverse and help people in it!")
.setThumbnail(Kiafumi.JDA.getSelfUser().getAvatarUrl())
.setTimestamp(ZonedDateTime.now())
.setFooter(EmbedUI.BRAND);
event.getChannel().sendMessageEmbeds(eb.build()).queue();
}
}
}

View File

@ -1,118 +0,0 @@
package moe.oko.Kiafumi.listener;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.KiafumiConfig;
import moe.oko.Kiafumi.util.EmbedUI;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import static moe.oko.Kiafumi.util.LoggingManager.debug;
import static moe.oko.Kiafumi.util.LoggingManager.info;
import static moe.oko.Kiafumi.util.LoggingManager.error;
/**
* Skynet Listener
* Handles all event logging (Join/Leaves, Username Change, etc).
* @author oko
*/
public class SkynetListener extends ListenerAdapter {
KiafumiConfig config;
DateTimeFormatter dTF = DateTimeFormatter.ofPattern("MM-dd-yyyy");
/**
* Join/Leave logging
* Requires the server to configure welcomeChannel (optionally joinRole).
*/
@Override
public void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) {
var guild = event.getGuild();
var server = Kiafumi.instance.getServerManager().getOrCreateServer(guild);
var member = event.getMember();
info("User [" + event.getUser().getName() + ":" + event.getUser().getId() + "] joined guild [" + guild.getName() + ':' + guild.getId() + "].");
if(server.getJoinRole() != null) {
try {
guild.addRoleToMember(member, guild.getRoleById(server.getJoinRole())).queue();
} catch(Exception ex) {
error("Failed to apply welcome role to " + member.getEffectiveName() + ", as the role does not exist.");
}
}
if(server.getWelcomeChannel() != null) {
// Fetch the welcome channel from settings.
TextChannel textChannel;
try {
textChannel = guild.getTextChannelById(server.getWelcomeChannel());
} catch (Exception ex) {
error("Failed to send join message to guild " + guild.getId() + " as the welcome channel was not found.");
return;
}
// Prepare embed.
var eb = new EmbedBuilder()
.setColor(EmbedUI.SUCCESS)
.setAuthor(member.getEffectiveName() + "#" + member.getUser().getDiscriminator() + " ("
+ member.getId() + ")", null, event.getUser().getAvatarUrl()) // Url cannot be member.
.setDescription(member.getAsMention() + " | **Joined Discord**: " + member.getTimeCreated().format(dTF))
.setFooter("User Joined")
.setTimestamp(OffsetDateTime.now());
textChannel.sendMessageEmbeds(eb.build()).queue();
debug("Guild join message successfully sent.");
}
}
@Override
public void onGuildMemberRemove(@NotNull GuildMemberRemoveEvent event) {
var guild = event.getGuild();
info("User [" + event.getUser().getName() + ":" + event.getUser().getId() + "] left guild [" + guild.getName() + ':' + guild.getId() + "].");
var server = Kiafumi.instance.getServerManager().getOrCreateServer(guild);
if(server.getWelcomeChannel() != null) {
// Fetch the welcome channel from settings.
TextChannel textChannel;
var member = event.getMember();
try {
textChannel = guild.getTextChannelById(server.getWelcomeChannel());
} catch (Exception ex) {
error("Failed to send leave message to guild " + guild.getId() + " as the welcome channel was not found.");
return;
}
// Prepare embed.
var eb = new EmbedBuilder()
.setColor(EmbedUI.FAILURE)
.setAuthor(member.getEffectiveName() + "#" + member.getUser().getDiscriminator() + " ("
+ member.getId() + ")", null, event.getUser().getAvatarUrl()) // Url cannot be member.
.setDescription(member.getAsMention() + " | **Joined Server**: " + member.getTimeJoined().format(dTF))
.setFooter("User Left")
.setTimestamp(OffsetDateTime.now());
textChannel.sendMessageEmbeds(eb.build()).queue();
debug("Guild leave message successfully sent.");
}
}
/**
* Name change logging
* Sent to the central logChannel.
*/
@Override
public void onUserUpdateName(@NotNull UserUpdateNameEvent event) {
var user = event.getUser();
String name = event.getNewName(), oldName = event.getOldName();
info("User [" + name + ':' + user.getId() + "] changed name from \"" + oldName + "\".");
var logChannel = Kiafumi.JDA.getTextChannelById(config.getLogChannel());
var eb = new EmbedBuilder()
.setColor(EmbedUI.INFO)
.setAuthor(user.getName() + '#' + user.getDiscriminator() + " (" + user.getId() + ')', null, user.getAvatarUrl())
.setDescription('`' + oldName + "` → `" + name + '`')
.setFooter("Username Edited")
.setTimestamp(OffsetDateTime.now());
logChannel.sendMessageEmbeds(eb.build()).queue();
debug("Username edit message successfully sent.");
}
}

View File

@ -1,176 +0,0 @@
package moe.oko.Kiafumi.model;
import moe.oko.Kiafumi.Kiafumi;
import javax.annotation.Nullable;
/**
* Server Class
* Used for in-memory data storage. Loaded from Database later.
* @author Kay
* @implNote This class is where all server info is stored per-server, so be liberal with additions.
*/
public class Server {
// Guild ID
private String id;
// The channel for welcome logs to be posted to.
private String welcomeChannel;
// The role to be assigned on join, if null ignored.
private String joinRole;
//Moderation role, used for /mod
private String modChannel;
//If the server has been modified in memory, for saving persistently.
private boolean modified;
/**
* Default constructor, used for new servers
* @param id - the guild id to have server constructed for.
*/
public Server(String id) {
this.id = id;
this.welcomeChannel = null;
this.modChannel = null;
this.joinRole = null;
this.modified = false;
}
/**
* Database Constructor
* @param id - id of the server
* @param welcomeChannel - channel for welcome messages, if enabled
*/
public Server(String id, @Nullable String welcomeChannel, @Nullable String joinRole, String modChannel) {
this.id = id;
this.welcomeChannel = welcomeChannel;
this.modChannel = modChannel;
this.joinRole = joinRole;
this.modified = false;
}
public String getId() { return id; }
public String getJoinRole() { return joinRole; }
public String getModChannel() { return modChannel; }
public String getWelcomeChannel() {
return welcomeChannel;
}
public void setJoinRole(String joinRole) {
this.modified = true;
this.joinRole = joinRole;
}
public void setWelcomeChannel(String welcomeChannel) {
this.modified = true;
this.welcomeChannel = welcomeChannel;
}
/**
* Checks the modification of the server file in memory
* @return - whether the server settings have been modified
*/
public boolean isModified() {
return modified;
}
/**
* Options in the server class that can be modified.
* @return - Options in a string that can be printed to discord.
*/
public String getOpts() {
return """
welcomeChannel - the channel to send welcome messages to
modChannel - the channel to send moderation logs to
joinRole - the role to apply to new members who join this guild""";
}
/**
* Fetches the option value by string for discord command.
* @param string - the string to assess
* @return - the value (if applicable) to return
*/
public String getOptionByString(String string) {
return switch (string.toLowerCase()) {
case "joinrole" -> joinRole;
case "welcomechannel" -> welcomeChannel;
case "modchannel" -> modChannel;
default -> "INVALID";
};
}
/**
* Resets an option based on the string provided
* @param name - name of the option to be reset
* @return - returns whether the function succeeded.
*/
public String resetOptionByString(String name) {
switch(name.toLowerCase()) {
case "joinrole":
joinRole = null;
return "Auto-role on join is now set to disabled (Default).";
case "welcomechannel":
welcomeChannel = null;
return "Welcome channel is now unset.";
case "modchannel":
modChannel = null;
return "Mod channel is now unset.";
default:
return "INVALID SETTING";
}
}
/**
* Sets an option by a string, if it can find one
* @param name - the name of the option
* @param value - the value to have the option set to
* @return - whether the name and value were valid and the option was set.
*/
public String setOptionByString(String name, String value) {
modified = true; // If this is being used set it to modified.
switch (name.toLowerCase()) {
case "joinrole":
try {
if (Kiafumi.JDA.getRoleById(value) == null) {
return "That role ID is invalid.";
} else {
joinRole = value;
return "Successfully set joinRole ID to " + value;
}
} catch (Exception ex) {
return "Bad Value";
}
case "welcomechannel":
try {
if (Kiafumi.JDA.getTextChannelById(value) == null) {
return "That channel ID is invalid.";
} else {
welcomeChannel = value;
return "Successfully set welcomeChannel ID to " + value;
}
} catch (Exception ex) {
return "Bad Value";
}
case "modchannel":
try {
if(Kiafumi.JDA.getRoleById(value) == null) {
return "That role ID is invalid.";
} else {
modChannel = value;
return "Successfully set modChannel ID to " + modChannel;
}
} catch (Exception ex) {
return "Bad Value";
}
default:
return "INVALID setting name.";
}
}
@Override
public String toString() {
return "Server [id=" + this.id + ",welcomeChannel="+welcomeChannel+
",joinrole=" + joinRole + ",modChannel=" + modChannel +"]";
}
}

View File

@ -1,80 +0,0 @@
package moe.oko.Kiafumi.model;
import moe.oko.Kiafumi.Kiafumi;
import net.dv8tion.jda.api.entities.Guild;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.*;
/**
* ServerManager Class
* Permits the access of servers easily
* @author Kay
*/
public class ServerManager {
//Server Memory Storage Hashmap, <Server/Guild ID, Server Object>
private HashMap<String, Server> servers = new HashMap<>();
/**
* Constructor, loads servers and initializes the hashmap.
*/
public ServerManager() {
List<Server> loadedServers = Kiafumi.instance.database.loadServerInformation();
if(loadedServers == null) {
error("Failed to load servers properly. Null val on database.");
return;
}
HashMap<String, Server> serverHashMap = new HashMap<>();
for(Server s : loadedServers) {
serverHashMap.put(s.getId(), s);
}
info("Successfully loaded " + serverHashMap.size() + " servers.");
servers = serverHashMap;
}
/**
* Fetches the server via the guild id
* @param id - the id to find a server for
* @return - the server, if non-existent a default profile is created for it.
*/
public Server getOrCreateServer(String id) {
if(servers.get(id) == null) { createNewDefaultServer(Kiafumi.JDA.getGuildById(id)); }
return servers.get(id);
}
/**
* Fetches the server via the guild.
* @param guild - the guild to find the server for
* @return - the server, if non-existent a default profile is created for it.
*/
public Server getOrCreateServer(Guild guild) {
if(servers.get(guild.getId()) == null) { createNewDefaultServer(guild); }
return servers.get(guild.getId());
}
/**
* Initializes a persistent profile for the server and creates a new one that is loaded into memory.
* @param guild - the guild to have a default profile created for it.
* @return - whether the function succeeded.
*/
public boolean createNewDefaultServer(Guild guild) {
var serverName = guild.getName() + ":" + guild.getId() + "].";
debug("Creating data for [" + serverName);
Server server = new Server(guild.getId());
if(!Kiafumi.instance.getDatabase().createServerInformation(guild)) {
error("Failed to create new defaults for " + guild.getId());
return false;
}
servers.put(server.getId(), server);
return true;
}
/**
* Returns all the servers loaded in memory. Used for database persistence.
* @return - the servers loaded in memory in a collection.
*/
public Collection<Server> getServers() { return servers.values(); }
}

View File

@ -1,47 +0,0 @@
package moe.oko.Kiafumi.model.audio;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import java.util.HashSet;
import java.util.Set;
/**
* Imported from SHIRO project.
* License for Kiafumi still applies.
* @author Kay
*/
public class AudioInfo {
private final AudioTrack track;
private final Set<String> skips;
private final Member author;
AudioInfo(AudioTrack track, Member author) {
this.track = track;
this.skips = new HashSet<>();
this.author = author;
}
public AudioTrack getTrack() {
return track;
}
public int getSkips() {
return skips.size();
}
public void addSkip(User u) {
skips.add(u.getId());
}
public boolean hasVoted(User u) {
return skips.contains(u.getId());
}
public Member getAuthor() {
return author;
}
}

View File

@ -1,37 +0,0 @@
package moe.oko.Kiafumi.model.audio;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import java.nio.ByteBuffer;
/**
* Imported from SHIRO project.
* License for Kiafumi still applies
* @author Kay
*/
public class AudioPlayerSendHandler implements AudioSendHandler {
private final AudioPlayer audioPlayer;
private AudioFrame lastFrame;
public AudioPlayerSendHandler(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
}
@Override
public boolean canProvide() {
lastFrame = audioPlayer.provide();
return lastFrame != null;
}
@Override
public ByteBuffer provide20MsAudio() {
return ByteBuffer.wrap(lastFrame.getData());
}
@Override
public boolean isOpus() {
return true;
}
}

View File

@ -1,89 +0,0 @@
package moe.oko.Kiafumi.model.audio;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
import net.dv8tion.jda.api.entities.AudioChannel;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Import from SHIRO project.
* License for Kiafumi still applies
* @author Kay
*/
public class TrackManager extends AudioEventAdapter {
private final AudioPlayer player;
private final Queue<AudioInfo> queue;
public TrackManager(AudioPlayer player) {
this.player = player;
this.queue = new LinkedBlockingQueue<>();
}
/**
* Queues a new track to be played.
* @param track - the track to be played
* @param author - the person who queued the track
*/
public void queue(AudioTrack track, Member author) {
AudioInfo info = new AudioInfo(track, author);
queue.add(info);
if (player.getPlayingTrack() == null) {
player.playTrack(track);
}
}
@Override
public void onTrackStart(AudioPlayer player, AudioTrack track) {
AudioInfo info = queue.element();
AudioChannel vChan = info.getAuthor().getVoiceState().getChannel();
if (vChan == null) { // User has left all voice channels
player.stopTrack();
} else {
info.getAuthor().getGuild().getAudioManager().openAudioConnection(vChan);
}
}
@Override
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
Guild g = queue.poll().getAuthor().getGuild();
if (queue.isEmpty()) {
g.getAudioManager().closeAudioConnection();
} else {
player.playTrack(queue.element().getTrack());
}
}
public void shuffleQueue() {
List<AudioInfo> tQueue = new ArrayList<>(getQueuedTracks());
AudioInfo current = tQueue.get(0);
tQueue.remove(0);
Collections.shuffle(tQueue);
tQueue.add(0, current);
purgeQueue();
queue.addAll(tQueue);
}
public Set<AudioInfo> getQueuedTracks() {
return new LinkedHashSet<>(queue);
}
public void purgeQueue() {
queue.clear();
}
public void remove(AudioInfo entry) {
queue.remove(entry);
}
public AudioInfo getTrackInfo(AudioTrack track) {
return queue.stream().filter(audioInfo -> audioInfo.getTrack().equals(track)).findFirst().orElse(null);
}
}

View File

@ -1,13 +0,0 @@
package moe.oko.Kiafumi.model.db;
public record DBCredentials(
String username,
String password,
String host,
int port,
String db,
int poolSize,
long connTimeout,
long idleTimeout,
long maxLifetime
) {}

View File

@ -1,178 +0,0 @@
package moe.oko.Kiafumi.model.db;
import com.google.common.base.Strings;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import moe.oko.Kiafumi.Kiafumi;
import moe.oko.Kiafumi.model.Server;
import net.dv8tion.jda.api.entities.Guild;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static moe.oko.Kiafumi.util.LoggingManager.*;
/**
* Kiafumi DB Class
* Basically our helpful MySQL functions that pertain to data persistence.
* @author Kay, with moral support from Yodabird
*/
public class KiafumiDB {
private HikariDataSource dataSource;
private Connection connection;
// The prepared statement strings
private final String CREATE_SERVERINFO_TABLE = "CREATE TABLE IF NOT EXISTS `serverInfo`" +
"(`id` LONGTEXT NOT NULL, `welcomeChannel` LONGTEXT NULL, `modChannel` LONGTEXT NULL, joinRole LONGTEXT NULL);";
private final String INSERT_SERVERINFO_DEFAULT = "INSERT INTO serverInfo(id, welcomeChannel, modChannel, joinRole) values (?,?,?,?)";
private final String SERVERINFO_EXISTS = "select * from serverInfo where id = ?";
private final String SERVER_INFO_LOAD = "select * from serverInfo";
// 1 = welcomeChannel, 2 = modChannel , 3 = joinRole, 4 = Guild_ID (immutable)
private final String SERVER_INFO_MODIFY = "UPDATE `serverInfo` SET welcomeChannel=?, modChannel=?, joinRole=? WHERE id = ?";
/**
* KiafumiDB Connection Constructor
*/
public KiafumiDB(DBCredentials credentials) {
var config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://%s:%s/%s".formatted(credentials.host(), credentials.port(), credentials.db()));
config.setConnectionTimeout(credentials.connTimeout());
config.setIdleTimeout(credentials.idleTimeout());
config.setMaxLifetime(credentials.maxLifetime());
config.setMaximumPoolSize(credentials.poolSize());
config.setUsername(credentials.username());
if (!Strings.isNullOrEmpty(credentials.password()))
config.setPassword(credentials.password());
this.dataSource = new HikariDataSource(config);
try {
connection = dataSource.getConnection();
info("Connected to database.");
} catch (SQLException e) {
error("Unable to connect to database.");
e.printStackTrace();
this.dataSource = null;
}
initializeTables();
}
/**
* Table initialization function, ran on every startup.
*/
private void initializeTables() {
try {
connection.prepareStatement(CREATE_SERVERINFO_TABLE).execute();
} catch (Exception ex) {
ex.printStackTrace();
error("Failed to initialize SQL tables. They may need to be created manually.");
}
}
/**
* Creates the default server information when the bot joins a discord.
* @param guild - the guild that the bot joined
* @return whether the function succeeded.
*/
public boolean createServerInformation(Guild guild) {
try {
PreparedStatement prep = connection.prepareStatement(SERVERINFO_EXISTS);
prep.setInt(1, (int) guild.getIdLong());
ResultSet rsCheck = prep.executeQuery();
while (rsCheck.next()) {
//Server already exists, no need to initialize a section for it.
if(rsCheck.getInt(1) != 0) {
info("Server already existed. Skipping initialization.");
return true;
}
}
//Proceed with making defaults.
PreparedStatement ps = connection.prepareStatement(INSERT_SERVERINFO_DEFAULT);
ps.setString(1, guild.getId());
ps.setNull(2, Types.LONGVARCHAR);
ps.setNull(3, Types.LONGVARCHAR);
ps.setNull(4, Types.LONGVARCHAR);
ps.execute();
return true;
} catch (Exception ex) {
error("Failed to create default server info for guild " + guild.getId());
ex.printStackTrace();
return false;
}
}
/**
* Loads all the server information from MySQL into memory.
* @return - a list of all servers loaded from MySQL.
*/
public List<Server> loadServerInformation() {
List<Server> servers = new ArrayList<>();
try {
PreparedStatement ps = connection.prepareStatement(SERVER_INFO_LOAD);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
info("Starting new load for server: " + rs.getString(1));
String id = rs.getString(1);
String welcomeChannel = rs.getString(2);
String modChannel = rs.getString(3);
String joinRole = rs.getString(4);
Server server = new Server(id, welcomeChannel, modChannel, joinRole);
debug("Loaded " + server + "from database.");
servers.add(server);
}
return servers;
} catch (Exception ex) {
ex.printStackTrace();
error("Failed to load server information, check stack.");
return null;
}
}
/**
* Saves modified servers into persistent MySQL server.
* @return - whether the method succeeded.
*/
public boolean saveServerInformation() {
Collection<Server> servers = Kiafumi.instance.getServerManager().getServers();
info("Starting save on " + servers.size() + " servers.");
try {
// 1 = welcomeChannel, 2 = modChannel, 3 = joinRole, 4 = Guild_ID (immutable)
PreparedStatement ps = connection.prepareStatement(SERVER_INFO_MODIFY);
int i = 0;
for (Server server : servers) {
if(!server.isModified()) { continue; } // Skip, unmodified server.
info("Starting save on modified " + server);
if(server.getWelcomeChannel() != null) {
ps.setString(1, server.getWelcomeChannel());
} else {
ps.setNull(1, Types.LONGVARCHAR);
}
if(server.getModChannel() != null) {
ps.setString(2, server.getModChannel());
} else {
ps.setNull(2, Types.LONGVARCHAR);
}
if(server.getJoinRole() != null) {
ps.setString(3, server.getJoinRole());
} else {
ps.setNull(3, Types.LONGVARCHAR);
}
ps.setString(4, server.getId());
ps.addBatch();
i++;
}
info("Total Batches: " + i + ". Starting SQL save.");
ps.executeBatch();
return true;
} catch (Exception ex) {
error("Failed to persist server data. Check stack.");
ex.printStackTrace();
return false;
}
}
}

View File

@ -1,87 +0,0 @@
package moe.oko.Kiafumi.util;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import java.util.HashMap;
/**
* Helpful CommandInfo class to easily make slash commands.
* @author Kay
*/
public class CommandInfo {
private String name;
private String description;
private CommandType type;
private HashMap<String, OptionType> options;
private HashMap<String, String> optionDescriptions;
private HashMap<String, Boolean> optionRequirements;
private HashMap<String, CommandInfo> subCommands;
/**
* Constructor to build CommandInfo with.
* @param name - Name of the slash command (MUST BE ALL LOWERCASE)
* @param description - Description of the slash command
*/
public CommandInfo(String name, String description, CommandType type) {
this.name = name;
this.description = description;
this.type = type;
this.options = new HashMap<>();
this.optionDescriptions = new HashMap<>();
this.optionRequirements = new HashMap<>();
this.subCommands = new HashMap<>();
}
/**
* Returns whether the command has options/input.
* @return - boolean
*/
public boolean hasOptions() {
return options.size() != 0;
}
public boolean hasSubCommands() { return subCommands.size() != 0; }
public String getDescription() {
return description;
}
public String getName() {
return name;
}
public HashMap<String, OptionType> getOptions() {
return options;
}
public HashMap<String, Boolean> getOptionRequirements() {
return optionRequirements;
}
public HashMap<String, String> getOptionDescriptions() {
return optionDescriptions;
}
public HashMap<String, CommandInfo> getSubCommands() { return subCommands; }
/**
* The way you add options to a command. Use this function for EACH argument.
* @param name - name of the field
* @param description - description for the field
* @param type - the OptionType of the field (e.x. OptionType.STRING, OptionType.CHANNEL, etc.)
* @param required - whether the command can be run without the field or not.
*/
public void addOption(String name, String description, OptionType type, boolean required) {
options.put(name, type);
optionDescriptions.put(name, description);
optionRequirements.put(name, required);
}
/**
* The way you add subcommands to a command. NOTE you cannot have options for the command if you use this.
* @param cmdInfo - The CommandInfo for the subcommand (including options).
*/
public void addSubcommand(CommandInfo cmdInfo) {
subCommands.put(cmdInfo.name, cmdInfo);
}
}

View File

@ -1,18 +0,0 @@
package moe.oko.Kiafumi.util;
/**
* Used to identify what type of Command is being used.
* This is intended to prevent JDA errors.
*/
public enum CommandType {
/**
* Commands that are registered using the upsertCommand method.
*/
COMMAND,
/**
* Commands that fall under above commands using the addSubcommand function.
*/
SUBCOMMAND
}

View File

@ -1,24 +0,0 @@
package moe.oko.Kiafumi.util;
import java.awt.Color;
/**
* EmbedUI Class
* @author oko
*/
public abstract class EmbedUI {
// TODO: restructure & rename class - it has surpassed its scope.
/**
* Shorthand reference for common EmbedBuilder colors & strings.
* I chose these colors based on the Pantone Color of the year.
*/
// Strings
public static final String BRAND = "Kiafumi - oko.moe";
public static final String RESPONSE_PRIVILEGES = " Insufficient privileges.";
// Colors
public static final Color SUCCESS = new Color(136,176,75);
public static final Color FAILURE = new Color(255,111,97);
public static final Color INFO = new Color(123,196,196);
}

View File

@ -1,48 +0,0 @@
package moe.oko.Kiafumi.util;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
/**
* Logging Class
* Provides static logging & extensible template messages.
* @author oko
*/
public class LoggingManager {
private static final Logger logger = LoggerFactory.getLogger("Kiafumi");
// Static logging reference
public static void debug(String str) { logger.debug(str); }
public static void info(String str) { logger.info(str); }
public static void warn(String str) { logger.warn(str); }
public static void error(String str) { logger.error(str); }
/**
* Used for logging commands with ease to console via a static method.
* @param event - the event ran
* @param msg - Any message to append with this.
*/
public static void slashLog(SlashCommandInteractionEvent event, @Nullable String msg) {
var user = event.getUser();
info("""
User [%s:%s] ran command: "%s" %s"""
.formatted(user.getName(), user.getId(), event.getName(), msg == null ? "" : msg));
}
public static void slashLog(SlashCommandInteractionEvent event) {
var user = event.getUser();
info("""
User [%s:%s] ran command: "%s"."""
.formatted(user.getName(), user.getId(), event.getName()));
}
public static void slashResponse(SlashCommandInteractionEvent event, String msg) {
var user = event.getUser();
info("""
User [%s:%s] was provided response: "%s\"""".formatted(user.getName(), user.getId(), msg));
}
}

View File

@ -1,27 +0,0 @@
package moe.oko.Kiafumi.util;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.util.EntityUtils;
/**
* Basic ResponseHandler Class
* Intended for GET requests from online API's
* Such as, Steam, DDG, ProtonDB, CatApi, etc.
*/
public class ResponseHandlers {
/**
* @apiNote Returns GET response in a uniformed string. Use JSONArray to convert it into JSON.
**/
public static final ResponseHandler<String> STRING_RESPONSE_HANDLER = response -> {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity) : null;
} else {
throw new ClientProtocolException("Unexpected response status: " + status);
}
};
}

View File

@ -1,54 +0,0 @@
# ██╗ ██╗██╗ █████╗ ███████╗██╗ ██╗███╗ ███╗██╗
# ██║ ██╔╝██║██╔══██╗██╔════╝██║ ██║████╗ ████║██║
# █████╔╝ ██║███████║█████╗ ██║ ██║██╔████╔██║██║
# ██╔═██╗ ██║██╔══██║██╔══╝ ██║ ██║██║╚██╔╝██║██║
# ██║ ██╗██║██║ ██║██║ ╚██████╔╝██║ ╚═╝ ██║██║
# ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝
# --------------------------------------------------------------
# KIAFUMI Default Configuration File (With Comments)
# --------------------------------------------------------------
# DISCORD CONFIG STACK
discord:
# Paste in your bot token here.
token: "DO NOT SHARE"
# The main guild the bot will recognize and do protection actions on.
mainGuild: ""
# The channel ID the bot will send important logging messages to.
logChannel: ""
# The user ID of the owner of the bot.
ownerId: ""
# The client ID of the bot (for invite generation)
clientId: ""
# Level of permissions for the invite (If you don't know this, use https://discordapi.com/permissions.html)
# If problems occur, you can use 8 (Administrator)
invitePermissionLevel: 139855252544
# MAIN CONFIG STACK
main:
# Activity Type (STREAMING|PLAYING|WATCHING|COMPETING|DEFAULT)
activityType: "PLAYING"
# Activity Message
activityMsg: "https://oko.moe"
# Status type (ONLINE|IDLE|DO_NOT_DISTURB|INVISIBLE)
statusType: "IDLE"
# Responses for the default ping command
pingResponses:
- "Pinged the pentagon"
- "Pinged oko's basement"
- "Pinged oko.moe"
- "Pinged you're mother"
- "Pinged 142.250.189.238"
- "Pinged cuomo.zone"
- "Pinged my brain"
- "Pinged 38.fail"
- "Pinged pong"
- "Pinged the admins"
sql:
host: "localhost"
port: 3306
database: "kiafumi"
username: "alpine"
password: "squidlover42069"
poolSize: 2
connectionTimeout: 10000
idleTimeout: 600000
maxLifetime: 7200000

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_PATTERN">%d{HH:mm} %highlight{%-5level} %cyan{%logger{11}} - %m%n</Property>
</Properties>
<Appenders>
<Console name="console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>