Skip to content

Commit

Permalink
Almost final bot version
Browse files Browse the repository at this point in the history
  • Loading branch information
Laxystem committed Dec 13, 2023
1 parent 17dcdf7 commit 991a0ca
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 49 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ val klogging: String by project
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$serialization")

implementation("com.kotlindiscord.kord.extensions:kord-extensions:$kordex")

Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/quest/laxla/supertrouper/AboutExtension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package quest.laxla.supertrouper

class AboutExtension : TrouperExtension() {
override suspend fun setup() {
TODO("Not yet implemented")
}

}
5 changes: 3 additions & 2 deletions src/main/kotlin/quest/laxla/supertrouper/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import kotlinx.coroutines.runBlocking
import quest.laxla.supertrouper.messaging.PrivateMassagingExtension

private val token = env("TOKEN")
private val testingServer = envOrNull("TESTING_SERVER")
val officialServer = env("OFFICIAL_SERVER")
val isDevelopmentEnvironment = envOrNull("IS_DEV_ENV").toBoolean()

fun main() = runBlocking {
ExtensibleBot(token) {
applicationCommands {
testingServer?.let { defaultGuild(it) }
if (isDevelopmentEnvironment) defaultGuild(officialServer)
}

extensions {
Expand Down
20 changes: 8 additions & 12 deletions src/main/kotlin/quest/laxla/supertrouper/MaintenanceExtension.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package quest.laxla.supertrouper

import com.kotlindiscord.kord.extensions.checks.isBotAdmin
import com.kotlindiscord.kord.extensions.commands.application.slash.publicSubCommand
import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand
import com.kotlindiscord.kord.extensions.extensions.slashCommandCheck
import dev.kord.common.entity.Snowflake

class MaintenanceExtension : TrouperExtension() {
override suspend fun setup() {
Expand All @@ -12,18 +12,14 @@ class MaintenanceExtension : TrouperExtension() {
}

publicSlashCommand {
name = "maintenance"
description = "Maintenance commands for maintainers of the bot"
name = "stop"
description = "Stops the bot completely"
guildId = Snowflake(officialServer)

publicSubCommand {
name = "stop"
description = "Stops the bot completely"

action {
//language=Markdown
respond { content = "# Invoking Protocol: Emergency Stop" }
bot.stop()
}
action {
//language=Markdown
respond { content = "# Invoking Protocol: Emergency Stop" }
bot.stop()
}
}
}
Expand Down
14 changes: 8 additions & 6 deletions src/main/kotlin/quest/laxla/supertrouper/Overwrites.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,32 @@ fun PermissionOverwritesBuilder.addOverwrite(

fun PermissionOverwritesBuilder.sync(
vararg overrides: Overwrite,
defaults: Iterable<PermissionOverwriteEntity>
) = sync(overrides.asIterable(), defaults)
defaults: Iterable<PermissionOverwriteEntity>,
neverAllow: Permissions = Permissions()
) = sync(overrides.asIterable(), defaults, neverAllow)

fun PermissionOverwritesBuilder.sync(
overrides: Iterable<Overwrite>,
defaults: Iterable<PermissionOverwriteEntity>
defaults: Iterable<PermissionOverwriteEntity>,
neverAllow: Permissions = Permissions()
) {
val permissions = mutableMapOf<Overwrite, PermissionOverwriteEntity>()

defaults.forEach { default ->
val override = overrides.find { it.id == default.target && it.type == default.type }

if (override == null) addOverwrite(default.target, default.type, default.allowed, default.denied)
if (override == null) addOverwrite(default.target, default.type, default.allowed - neverAllow, default.denied)
else permissions[override] = default
}

overrides.forEach { override ->
val default = permissions[override]

if (default == null) addOverwrite(override)
if (default == null) addOverwrite(override.copy(allow = override.allow - neverAllow))
else addOverwrite(
default.target,
default.type,
default.allowed - default.denied - override.deny + override.allow,
default.allowed - default.denied + override.allow - override.deny - neverAllow,
default.denied - default.allowed - override.allow + override.deny
)
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/quest/laxla/supertrouper/TargetedArguments.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package quest.laxla.supertrouper

import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommandContext
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalMember
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalUser
import com.kotlindiscord.kord.extensions.components.forms.ModalForm

private const val TargetArgumentName = "target"
private const val TargetArgumentDescription = "Target of this command. Defaults to you."

open class TargetedArguments : Arguments() {
val targetOrNull by optionalUser {
val targetOrNull by optionalMember {
name = TargetArgumentName
description = TargetArgumentDescription
}
}

val <C, A, M> C.target where C : SlashCommandContext<*, A, M>, A : TargetedArguments, M : ModalForm
get() = arguments.targetOrNull ?: user
get() = arguments.targetOrNull ?: member!!
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,32 @@ import com.kotlindiscord.kord.extensions.checks.anyGuild
import com.kotlindiscord.kord.extensions.checks.isNotBot
import com.kotlindiscord.kord.extensions.extensions.*
import com.kotlindiscord.kord.extensions.types.EphemeralInteractionContext
import dev.kord.common.entity.OverwriteType
import dev.kord.common.entity.Permission
import com.kotlindiscord.kord.extensions.utils.any
import dev.kord.common.entity.*
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.behavior.channel.asChannelOfOrNull
import dev.kord.core.behavior.channel.createMessage
import dev.kord.core.behavior.channel.createTextChannel
import dev.kord.core.behavior.channel.edit
import dev.kord.core.behavior.createCategory
import dev.kord.core.behavior.interaction.respondEphemeral
import dev.kord.core.behavior.interaction.respondPublic
import dev.kord.core.entity.Member
import dev.kord.core.entity.Role
import dev.kord.core.entity.User
import dev.kord.core.entity.channel.Category
import dev.kord.core.entity.channel.TextChannel
import dev.kord.core.event.guild.MemberJoinEvent
import dev.kord.core.event.interaction.GuildButtonInteractionCreateEvent
import dev.kord.core.event.interaction.GuildSelectMenuInteractionCreateEvent
import dev.kord.gateway.Intent
import dev.kord.gateway.PrivilegedIntent
import dev.kord.rest.builder.channel.addMemberOverwrite
import dev.kord.rest.builder.channel.addRoleOverwrite
import dev.kord.rest.builder.message.actionRow
import dev.kord.rest.builder.message.create.AbstractMessageCreateBuilder
import dev.kord.rest.builder.message.embed
import kotlinx.coroutines.flow.count
import quest.laxla.supertrouper.*

Expand All @@ -31,10 +43,8 @@ class PrivateMassagingExtension : TrouperExtension() {

event<MemberJoinEvent> {
action {
if (event.member.isEligible && event.guild.members.count() < memberLimit) getOrCreateChannel(
getOrCreateCategory(event.guild),
event.member
)
if (event.member.isEligible && event.guild.members.count() < memberLimit)
getOrCreateChannel(getOrCreateCategory(event.guild), event.member)
}
}

Expand All @@ -43,19 +53,19 @@ class PrivateMassagingExtension : TrouperExtension() {
description = "Get a link to a user's private messages channel"

action {
executeFindCommand(getOrCreateCategory(guild!!), target.asUser())
executeFindCommand(getOrCreateCategory(guild!!), target.asUser(), user)
}
}

ephemeralUserCommand {
name = "Private Message"

action {
executeFindCommand(getOrCreateCategory(guild!!), targetUsers.single())
executeFindCommand(getOrCreateCategory(guild!!), targetUsers.single(), user)
}
}

ephemeralSlashCommand(::TargetedArguments) slash@{
ephemeralSlashCommand(::TargetedArguments) {
name = "sync"
description = "Syncs a private message channel's permissions with the category"

Expand All @@ -75,6 +85,16 @@ class PrivateMassagingExtension : TrouperExtension() {
executeSyncCommand(getOrCreateCategory(guild!!), user.asUser())
}
}

event<GuildButtonInteractionCreateEvent> {
action {
when (event.interaction.componentId) {
PingButton -> event.interaction.respondPublic {
executePingCommand(event.interaction.channel.asChannel(), event.interaction.user)
}
}
}
}
}

private suspend fun EphemeralInteractionContext.executeSyncCommand(category: Category, user: User) {
Expand All @@ -88,12 +108,13 @@ class PrivateMassagingExtension : TrouperExtension() {
val channelMention = channel.mention

channel.edit {
reason = "Syncing $userMention"
reason = "Sync $channelMention with category for $userMention"

sync(
overwrite(kord.selfId, OverwriteType.Member, allowed = privateMessageBotPermissions),
overwrite(user.id, OverwriteType.Member, allowed = privateMessageOwnerPermissions),
defaults = category.permissionOverwrites
overwrite(kord.selfId, OverwriteType.Member, allowed = pmBotPermissions),
overwrite(user.id, OverwriteType.Member, allowed = pmMemberPermissions),
defaults = category.permissionOverwrites,
neverAllow = kord.getSelf().asMember(category.guildId).getDeniedPermissions()
)
}

Expand All @@ -102,10 +123,14 @@ class PrivateMassagingExtension : TrouperExtension() {
}
}

private suspend fun EphemeralInteractionContext.executeFindCommand(category: Category, user: User) {
private suspend fun EphemeralInteractionContext.executeFindCommand(
category: Category, user: User, searcher: UserBehavior = user
) {
if (user.isEligible) {
val channel = getOrCreateChannel(category, user)
respond { content = channel.mention }

channel.ping(searcher)
} else respond {
content = user.mention + " is not eligible for private messaging."
}
Expand All @@ -118,7 +143,7 @@ class PrivateMassagingExtension : TrouperExtension() {
nsfw = false

addMemberOverwrite(kord.selfId) {
allowed += privateMessageBotPermissions
allowed += pmBotPermissions
}

addRoleOverwrite(guild.id) {
Expand All @@ -135,15 +160,34 @@ class PrivateMassagingExtension : TrouperExtension() {
val channel = category.createTextChannel(user.username) {
reason = "Created a PM with $mention."
nsfw = category.data.nsfw.discordBoolean
topic = "This channel is $mention's private message."
topic = "$mention's private messaging channel."

sync(
overwrite(kord.selfId, OverwriteType.Member, allowed = privateMessageBotPermissions),
overwrite(user.id, OverwriteType.Member, allowed = privateMessageOwnerPermissions),
defaults = category.permissionOverwrites
overwrite(kord.selfId, OverwriteType.Member, allowed = pmBotPermissions),
overwrite(user.id, OverwriteType.Member, allowed = pmMemberPermissions),
defaults = category.permissionOverwrites,
neverAllow = kord.getSelf().asMember(category.guildId).getDeniedPermissions()
)
}

val avatar = (user.avatar ?: user.defaultAvatar).cdnUrl.toUrl()

channel.createMessage {
embed {
description = "# $mention"
thumbnail { url = avatar }
}

actionRow {
interactionButton(ButtonStyle.Primary, customId = PingButton) {
label = "Ping"
emoji = DiscordPartialEmoji(name = "\uD83D\uDD14")
}
}
}

channel.ping(user)

return channel
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
package quest.laxla.supertrouper.messaging

import com.kotlindiscord.kord.extensions.utils.any
import com.kotlindiscord.kord.extensions.utils.envOrNull
import dev.kord.common.entity.ALL
import dev.kord.common.entity.Permission
import dev.kord.common.entity.Permissions
import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.behavior.channel.MessageChannelBehavior
import dev.kord.core.behavior.channel.createMessage
import dev.kord.core.entity.Member
import dev.kord.core.entity.User
import dev.kord.core.entity.channel.Category
import dev.kord.core.entity.channel.MessageChannel
import dev.kord.core.entity.channel.TextChannel
import dev.kord.rest.builder.message.allowedMentions
import dev.kord.rest.builder.message.create.AbstractMessageCreateBuilder
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.firstOrNull

const val PrivateMessagesCategoryName = "Private Messages"
const val InviteSelect = "PM.invite"
const val PingButton = "PM.ping"
const val ManagePmButton = "PM.manage"
const val UserIdCapturingGroup = "userID"
val memberLimit = envOrNull("AUTOMATIC_CHANNEL_CREATION_MEMBER_LIMIT")?.toInt() ?: 30
val privateMessageOwnerPermissions = Permission.ViewChannel + Permission.ReadMessageHistory
val privateMessageBotPermissions =
privateMessageOwnerPermissions + Permission.ManageChannels + Permission.SendMessages + Permission.ManageMessages
val pmMemberPermissions = Permission.ViewChannel + Permission.ReadMessageHistory
val pmBotPermissions =
pmMemberPermissions + Permission.ManageChannels + Permission.SendMessages + Permission.ManageMessages
val userMentionRegex = "<@(?<$UserIdCapturingGroup>[1-9][0-9]+)>".toRegex()

suspend infix fun TextChannel.isOf(user: UserBehavior) = topic?.contains(user.mention) == true || pinnedMessages.any {
it.author?.id == kord.selfId && it.mentionedUserIds.singleOrNull() == user.id
}
infix fun TextChannel.isOf(user: UserBehavior) = topic?.contains(user.mention) == true

suspend fun getChannel(category: Category, user: User) = category.channels.filterIsInstance<TextChannel>().firstOrNull {
it.categoryId == category.id && it isOf user
Expand All @@ -30,3 +41,36 @@ suspend fun getCategory(guild: GuildBehavior) =

val User.isEligible get() = !isBot
val Category.isUsableForPrivateMessaging get() = name.equals(PrivateMessagesCategoryName, ignoreCase = true)

fun AbstractMessageCreateBuilder.executePingCommand(
channel: MessageChannel,
pinger: UserBehavior
) {
val owners = channel.owners?.toList()

if (owners == null) {
allowedMentions()

content = "The owner of this channel is unknown. They need to be mentioned in the channel's topic, " +
"Like this: `<@userID>`."
} else {
allowedMentions {
users.addAll(owners.asSequence().map {
Snowflake(it.groups[UserIdCapturingGroup]!!.value)
})
}

content = "Hey, " + owners.joinToString(separator = " ") {
it.value
} + ", y'all were pinged by " + pinger.mention + '!'
}
}

suspend fun MessageChannelBehavior.ping(user: UserBehavior) = createMessage {
allowedMentions { users.add(user.id) }
content = user.mention
}.delete(reason = "Ghost pinged " + user.mention)

val MessageChannel.owners get() = data.topic.value?.let { userMentionRegex.findAll(it) }

suspend fun Member.getDeniedPermissions() = Permissions.ALL - getPermissions()

0 comments on commit 991a0ca

Please sign in to comment.