Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/BotConfig.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package at.hannibal2.skyhanni.discord

import com.google.gson.GsonBuilder
import org.slf4j.LoggerFactory
import java.io.File
Expand All @@ -7,6 +8,7 @@ import kotlin.system.exitProcess
data class BotConfig(
val token: String,
val botCommandChannelId: String,
val moderationChannelId: String,
val allowedServerId: String,
val githubTokenOwn: String,
val githubTokenPullRequests: String,
Expand All @@ -15,24 +17,29 @@ data class BotConfig(

object ConfigLoader {
private val gson = GsonBuilder().setPrettyPrinting().create()
private val logger = LoggerFactory.getLogger(ConfigLoader::class.java)
private val logger = LoggerFactory.getLogger(ConfigLoader::class.java)
private val exampleConfig = BotConfig(
"TODO: discord token",
"TODO: staff channel id",
"TODO: allowed server id",
"TODO: github token with sh bot repo access",
"TODO: github token with sh mod repo access",
mapOf(
"user friendly (non important) name" to "TODO: role id"
)
)
fun load(filePath: String): BotConfig {
try {
val json = File(filePath).readText()
return gson.fromJson(json, BotConfig::class.java)
} catch (ex: Exception) {
logger.error("Could not load config. Below is an example config:\n```json\n${gson.toJson(exampleConfig)}\n```", ex)
exitProcess(1)
}
"TODO: discord token",
"TODO: staff channel id",
"TODO: moderation channel id",
"TODO: allowed server id",
"TODO: github token with sh bot repo access",
"TODO: github token with sh mod repo access",
mapOf(
"user friendly (non important) name" to "TODO: role id"
)
)

fun load(filePath: String): BotConfig {
try {
val json = File(filePath).readText()
return gson.fromJson(json, BotConfig::class.java)
} catch (ex: Exception) {
logger.error(
"Could not load config. Below is an example config:\n```json\n${gson.toJson(exampleConfig)}\n```",
ex
)
exitProcess(1)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private fun startBot(): DiscordBot {

val jda = JDABuilder.createDefault(token).also { builder ->
builder.enableIntents(GatewayIntent.MESSAGE_CONTENT)
builder.enableIntents(GatewayIntent.GUILD_MEMBERS)
builder.setEnableShutdownHook(false)
}.build()

Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import net.dv8tion.jda.api.utils.FileUpload
import java.awt.Color
import java.awt.Toolkit.getDefaultToolkit
import java.io.File
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.zip.ZipFile
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
Expand Down Expand Up @@ -68,6 +70,10 @@ object Utils {
}
}

fun MessageChannel.embedSend(embed: MessageEmbed) {
sendMessageEmbeds(embed).queue()
}

fun Message.replyWithConsumer(text: String, consumer: (MessageReceivedEvent) -> Unit) {
BotMessageHandler.log(text, consumer)
messageReply(text)
Expand Down Expand Up @@ -165,6 +171,19 @@ object Utils {

}

fun parseToUnixTime(isoTimestamp: String): Long =
Instant.from(DateTimeFormatter.ISO_INSTANT.parse(isoTimestamp)).epochSecond

fun passedSince(stringTime: String): String = "<t:${parseToUnixTime(stringTime)}:R>"

private val mentionRegex = Regex("<?@?(?<id>\\d+)>?")

fun String.getId(): String? {
return mentionRegex.matchEntire(this)?.let { result ->
result.groups["id"]?.value
}
}

fun String.linkTo(link: String): String = "[$this](<$link>)"

// keep comments as docs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package at.hannibal2.skyhanni.discord.command


import at.hannibal2.skyhanni.discord.BOT
import at.hannibal2.skyhanni.discord.BIG_X
import at.hannibal2.skyhanni.discord.Option
import at.hannibal2.skyhanni.discord.PLEADING_FACE
import at.hannibal2.skyhanni.discord.SimpleTimeMark
import at.hannibal2.skyhanni.discord.Utils.embed
import at.hannibal2.skyhanni.discord.Utils.embedSend
import at.hannibal2.skyhanni.discord.Utils.getId
import at.hannibal2.skyhanni.discord.Utils.messageSend
import at.hannibal2.skyhanni.discord.Utils.reply
import at.hannibal2.skyhanni.discord.Utils.userError
import at.hannibal2.skyhanni.discord.command.ManageCommands.createModerationEmbed
import at.hannibal2.skyhanni.discord.command.ManageCommands.handleError
import at.hannibal2.skyhanni.discord.command.ManageCommands.sendDM
import at.hannibal2.skyhanni.discord.command.PullRequestCommand.passedSince
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.Permission
import net.dv8tion.jda.api.entities.Member
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.User
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.exceptions.ErrorResponseException
import net.dv8tion.jda.api.requests.ErrorResponse
import java.awt.Color
import java.util.concurrent.TimeUnit

object ManageCommands {
fun createModerationEmbed(
action: String,
target: User,
moderator: Member,
reason: String,
color: Color = Color.RED
): MessageEmbed {
return EmbedBuilder()
.setAuthor("${target.name} was $action", null, target.avatarUrl)
.addField("Mention", target.asMention, false)
.addField("Moderator", moderator.asMention, false)
.addField("Reason", reason, false)
.addField("Time", passedSince(SimpleTimeMark.now().toString()), false)
.setColor(color)
.build()
}

fun handleError(error: Throwable): String {
return when (error) {
is ErrorResponseException -> {
when (error.errorResponse) {
ErrorResponse.UNKNOWN_MEMBER ->
"User not in guild!"

ErrorResponse.UNKNOWN_USER ->
"User doesn't exist!"

ErrorResponse.CANNOT_SEND_TO_USER ->
"Couldn't DM user!"

else -> "Discord API error: ${error.errorResponse}"
}
}

else -> "${error.message}"
}
}

fun User.sendDM(channel: MessageChannel, action: String, reason: String, color: Color = Color.RED) {
openPrivateChannel().queue { dm ->
val embed = embed("You were $action!", "**Reason:** $reason", color)

dm.sendMessageEmbeds(embed).queue(
{ channel.messageSend("Sent DM to ${name}.") },
{ error -> channel.messageSend(handleError(error)) }
)
}
}
}

@Suppress("unused")
class KickCommand : BaseCommand() {
override val name: String = "kick"
override val description: String = "Kicks a member from the server."
override val options: List<Option> = listOf(
Option("user", "The @ or id of the member you want to kick."),
Option("reason", "The reason why this member is being kicked.")
)

override fun MessageReceivedEvent.execute(args: List<String>) {
val mod = member ?: return reply("Internal error getting moderator.")
if (!mod.hasPermission(Permission.KICK_MEMBERS) && !mod.hasPermission(Permission.ADMINISTRATOR))
return reply("No perms $PLEADING_FACE")

val targetId = args[0].getId() ?: return userError("Invalid user! Did you enter the correct id?")
val reason = args.drop(1).joinToString(" ")

guild.retrieveMemberById(targetId).queue(
{ target ->
val modChannel = guild.getTextChannelById(BOT.config.moderationChannelId) ?: channel

target.user.sendDM(modChannel, "kicked", reason)

target.kick().reason(reason).queue {
modChannel.embedSend(createModerationEmbed("kicked", target.user, mod, reason))
}
},
{ error -> reply("$BIG_X ${handleError(error)}") }
)
}
}

@Suppress("unused")
class BanCommand : BaseCommand() {
override val name: String = "ban"
override val description: String = "Bans a member from the server."
override val options: List<Option> = listOf(
Option("user", "The @ or id of the member you want to ban."),
Option("timeframe", "The timeframe (in days, max 7) where messages should be deleted in."),
Option("reason", "The reason why this member is being banned.")
)

override fun MessageReceivedEvent.execute(args: List<String>) {
val mod = member ?: return reply("Internal error getting moderator.")
if (!mod.hasPermission(Permission.BAN_MEMBERS) && !mod.hasPermission(Permission.ADMINISTRATOR))
return reply("No perms $PLEADING_FACE")

val targetId = args[0].getId() ?: return userError("Invalid user! Did you enter the correct id?")
val timeframe = args[1].toIntOrNull().takeIf { it != null && it <= 7 }
?: return userError("Invalid timeframe! Did you enter a number <= 7?")
val reason = args.drop(2).joinToString(" ")

guild.retrieveMemberById(targetId).queue(
{ target ->
val modChannel = guild.getTextChannelById(BOT.config.moderationChannelId) ?: channel

target.user.sendDM(modChannel, "banned", reason)

target.ban(timeframe, TimeUnit.DAYS).reason(reason).queue {
modChannel.embedSend(createModerationEmbed("banned", target.user, mod, reason))
}
},
{ error -> reply("$BIG_X ${handleError(error)}") }
)
}
}

@Suppress("unused")
class UnbanCommand : BaseCommand() {
override val name: String = "unban"
override val description: String = "Unbans a member from the server."
override val options: List<Option> = listOf(
Option("user", "The @ or id of the member you want to unban."),
Option("reason", "The reason why this member is being unbanned.")
)

override fun MessageReceivedEvent.execute(args: List<String>) {
val mod = member ?: return reply("Internal error getting moderator.")
if (!mod.hasPermission(Permission.BAN_MEMBERS) && !mod.hasPermission(Permission.ADMINISTRATOR))
return reply("No perms $PLEADING_FACE")

val targetId = args[0].getId() ?: return userError("Invalid user! Did you enter the correct id?")
val reason = args.drop(1).joinToString(" ")

jda.retrieveUserById(targetId).queue(
{ target ->
val modChannel = guild.getTextChannelById(BOT.config.moderationChannelId) ?: channel

target.sendDM(modChannel, "unbanned", reason)

guild.unban(target).reason(reason).queue {
modChannel.embedSend(
createModerationEmbed("unbanned", target, mod, reason, color = Color.GREEN)
)
}
},
{ error -> reply("$BIG_X ${handleError(error)}") }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import at.hannibal2.skyhanni.discord.Utils.format
import at.hannibal2.skyhanni.discord.Utils.linkTo
import at.hannibal2.skyhanni.discord.Utils.logAction
import at.hannibal2.skyhanni.discord.Utils.messageDelete
import at.hannibal2.skyhanni.discord.Utils.parseToUnixTime
import at.hannibal2.skyhanni.discord.Utils.passedSince
import at.hannibal2.skyhanni.discord.Utils.reply
import at.hannibal2.skyhanni.discord.Utils.replyWithConsumer
import at.hannibal2.skyhanni.discord.Utils.runDelayed
Expand All @@ -27,8 +29,6 @@ import at.hannibal2.skyhanni.discord.json.discord.RunStatus
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import java.awt.Color
import java.io.File
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds

Expand Down Expand Up @@ -242,7 +242,7 @@ object PullRequestCommand : BaseCommand() {
stringBuilder: StringBuilder,
suffix: String = ""
): StringBuilder {
val labelsWithType = labels.intersect(labelTypes[labelType] ?: setOf())
val labelsWithType = labels.intersect((labelTypes[labelType] ?: setOf()).toSet())
if (labelsWithType.isEmpty()) return stringBuilder.append(if (suffix.isNotEmpty()) "> $labelType: $suffix\n" else "")
return stringBuilder.append("> $labelType: `${labelsWithType.joinToString("` `")}`$suffix\n")
}
Expand All @@ -254,13 +254,8 @@ object PullRequestCommand : BaseCommand() {
else -> Color(52, 125, 57)
}

private fun parseToUnixTime(isoTimestamp: String): Long =
Instant.from(DateTimeFormatter.ISO_INSTANT.parse(isoTimestamp)).epochSecond

private fun toTimeMark(stringTime: String): SimpleTimeMark = (parseToUnixTime(stringTime) * 1000).asTimeMark()

private fun passedSince(stringTime: String): String = "<t:${parseToUnixTime(stringTime)}:R>"

private fun releaseSinceMerge(stringTimeMerge: String, stringTimeLastRelease: String): Boolean {
val timeMerge = parseToUnixTime(stringTimeMerge)
val timeLastRelease = parseToUnixTime(stringTimeLastRelease)
Expand Down