Skip to content

Commit

Permalink
Merge branch 'root' into module/voice-rooms
Browse files Browse the repository at this point in the history
  • Loading branch information
gdude2002 committed May 7, 2023
2 parents c93c08f + 4830087 commit 406473a
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 17 deletions.
10 changes: 10 additions & 0 deletions src/main/kotlin/org/quiltmc/community/_Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,13 @@ internal val COMMUNITY_RELEASE_CHANNELS = envOrNull("COMMUNITY_RELEASE_CHANNELS"
?.split(',')
?.map { Snowflake(it.trim()) }
?: listOf()

internal val DEVLOG_CHANNEL = Snowflake(
envOrNull("DEVLOG_CHANNEL")?.toLong()
?: 908399987099045999
)

internal val DEVLOG_FORUM = Snowflake(
envOrNull("DEVLOG_FORUM")?.toLong()
?: 1103979333627936818
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,67 @@

package org.quiltmc.community.modes.quilt.extensions

import com.kotlindiscord.kord.extensions.checks.hasRole
import com.kotlindiscord.kord.extensions.checks.or
import com.kotlindiscord.kord.extensions.DiscordRelayedException
import com.kotlindiscord.kord.extensions.checks.*
import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand
import com.kotlindiscord.kord.extensions.commands.converters.impl.channel
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalChannel
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalTag
import com.kotlindiscord.kord.extensions.commands.converters.impl.tag
import com.kotlindiscord.kord.extensions.components.forms.ModalForm
import com.kotlindiscord.kord.extensions.extensions.Extension
import com.kotlindiscord.kord.extensions.extensions.ephemeralMessageCommand
import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand
import com.kotlindiscord.kord.extensions.extensions.event
import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI
import com.kotlindiscord.kord.extensions.modules.unsafe.extensions.unsafeSubCommand
import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashCommandResponse
import com.kotlindiscord.kord.extensions.modules.unsafe.types.ackEphemeral
import com.kotlindiscord.kord.extensions.types.respond
import com.kotlindiscord.kord.extensions.utils.addReaction
import com.kotlindiscord.kord.extensions.utils.ensureWebhook
import com.kotlindiscord.kord.extensions.utils.extraData
import dev.kord.common.annotation.KordUnsafe
import dev.kord.common.entity.ChannelFlag
import dev.kord.common.entity.ChannelType
import dev.kord.core.behavior.channel.asChannelOf
import dev.kord.core.behavior.channel.asChannelOfOrNull
import dev.kord.core.behavior.channel.createMessage
import dev.kord.core.behavior.channel.threads.edit
import dev.kord.core.behavior.edit
import dev.kord.core.behavior.execute
import dev.kord.core.behavior.interaction.modal
import dev.kord.core.behavior.interaction.response.createEphemeralFollowup
import dev.kord.core.entity.Message
import dev.kord.core.entity.channel.ForumChannel
import dev.kord.core.entity.channel.NewsChannel
import dev.kord.core.entity.channel.TopGuildMessageChannel
import dev.kord.core.entity.channel.thread.TextChannelThread
import dev.kord.core.entity.channel.thread.ThreadChannel
import dev.kord.core.event.message.MessageCreateEvent
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.coroutines.delay
import org.koin.core.component.inject
import org.quiltmc.community.*
import org.quiltmc.community.database.collections.UserFlagsCollection
import org.quiltmc.community.database.entities.UserFlags
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

private const val TOOLCHAIN_LOGO: String =
"https://raw.githubusercontent.com/QuiltMC/art/master/brand/512png/quilt_toolchain_logo_dark.png"

private val ICON_URL_REGEX = Regex("(?:^|\n)Icon URL:([^\n]+)")

class ForumExtension : Extension() {
override val name: String = "forum"

private val userFlags: UserFlagsCollection by inject()

override suspend fun setup() {
ephemeralSlashCommand {
name = "forum"
Expand All @@ -58,7 +87,7 @@ class ForumExtension : Extension() {
}
}

unsafeSubCommand(::ForumChannelArgs) {
unsafeSubCommand(::CreatePostArgs) {
name = "create-post"
description = "Create a Cozy-managed forum post"

Expand All @@ -76,6 +105,14 @@ class ForumExtension : Extension() {
return@action
}

if (forum.flags?.contains(ChannelFlag.RequireTag) == true && arguments.tag == null) {
ackEphemeral {
content = "This forum requires that you provide an initial tag for the post."
}

return@action
}

if (isDeveloper && forum.categoryId != COMMUNITY_DEVELOPER_CATEGORY) {
ackEphemeral {
content = "Quilt Developers may only use this command to create posts in the developer " +
Expand Down Expand Up @@ -112,9 +149,11 @@ class ForumExtension : Extension() {
}

val thread = forum.startPublicThread(title) {
// TODO: Tags?

message(text)

if (arguments.tag != null) {
appliedTags = mutableListOf(arguments.tag!!.id)
}
}

interactionResponse.createEphemeralFollowup {
Expand Down Expand Up @@ -147,7 +186,7 @@ class ForumExtension : Extension() {
}
}

unsafeSubCommand(::ForumPostArgs) {
unsafeSubCommand(::EditPostArgs) {
name = "edit-post"
description = "Edit an existing Cozy-managed forum post"

Expand All @@ -165,16 +204,16 @@ class ForumExtension : Extension() {

if (parent == null) {
ackEphemeral {
content = "Please provide a forum thread to edit the first post for, or run this " +
"command directly within the thread."
content = "Please provide a forum post to edit the first post for, or run this " +
"command directly within the post thread."
}

return@action
}

if (isDeveloper && parent.categoryId != COMMUNITY_DEVELOPER_CATEGORY) {
ackEphemeral {
content = "Quilt Developers may only use this command for threads in the developer " +
content = "Quilt Developers may only use this command for posts in the developer " +
"forum channels."
}

Expand All @@ -185,7 +224,7 @@ class ForumExtension : Extension() {

if (firstMessage == null) {
ackEphemeral {
content = "Unable to find the first message for this thread - is it a Cozy-managed thread?"
content = "Unable to find the first message for this post - is it a Cozy-managed post?"
}

return@action
Expand Down Expand Up @@ -230,19 +269,235 @@ class ForumExtension : Extension() {
}
}
}

ephemeralSubCommand(::PostTagArgs) {
name = "add-tag"
description = "Add a tag to the given post"

action {
val post = arguments.post.asChannelOfOrNull<TextChannelThread>()
val parent = post?.parent?.asChannelOfOrNull<ForumChannel>()

if (parent == null) {
respond {
content = "Please provide a forum post to edit the tags for."
}

return@action
}

if (post.appliedTags.contains(arguments.tag.id)) {
respond {
content = "This post already has that tag applied."
}

return@action
}

post.edit {
appliedTags = post.appliedTags.toMutableList()
appliedTags?.add(arguments.tag.id)
}

respond {
content = "Post tags updated."
}
}
}

ephemeralSubCommand(::PostTagArgs) {
name = "remove-tag"
description = "Remove a tag from the given post"

action {
val post = arguments.post.asChannelOfOrNull<TextChannelThread>()
val parent = post?.parent?.asChannelOfOrNull<ForumChannel>()

if (parent == null) {
respond {
content = "Please provide a forum post to edit the tags for."
}

return@action
}

if (!post.appliedTags.contains(arguments.tag.id)) {
respond {
content = "This post doesn't have that tag applied."
}

return@action
}

if (parent.flags?.contains(ChannelFlag.RequireTag) == true && post.appliedTags.size < 2) {
respond {
content = "You may not remove the last tag for a post in this forum."
}

return@action
}

post.edit {
appliedTags = post.appliedTags.toMutableList()
appliedTags?.remove(arguments.tag.id)
}

respond {
content = "Post tags updated."
}
}
}
}

ephemeralMessageCommand {
name = "Publish devlog"

check {
hasBaseModeratorRole(true)

or {
hasRole(COMMUNITY_DEVELOPER_ROLE)

if (passed) {
event.extraData["isDeveloper"] = true
}
}
}

check {
val parent = topChannelFor(event)
val message = messageFor(event)

if (
parent?.id != DEVLOG_FORUM // Wrong forum channel
) {
fail("This command may only be run in <#$DEVLOG_FORUM>.")

return@check
}

if (
message?.asMessageOrNull()?.author?.id == kord.selfId // Cozy sent the message
) {
fail("You may not publish a message posted by <@${kord.selfId}>.")

return@check
}

pass()
}

action {
targetMessages.first().publishDevlog()

respond {
content = "Message published."
}
}
}

event<MessageCreateEvent> {
check {
val parent = topChannelFor(event)
val message = messageFor(event)
val user = userFor(event)

if (
parent?.id != DEVLOG_FORUM || // Wrong forum channel
message?.asMessageOrNull()?.author?.id == kord.selfId || // Cozy sent the message
user == null // No user for the event
) {
fail()

return@check
}

val flags = userFlags.get(user.id) ?: UserFlags(user.id)

if (!flags.autoPublish) { // User settings set to not auto-publish
fail()

return@check
}

pass()
}

action {
event.message.publishDevlog()
}
}
}

private suspend fun Message.publishDevlog() {
val publishingChannel = kord.getChannelOf<TopGuildMessageChannel>(DEVLOG_CHANNEL)
?: throw DiscordRelayedException("Unable to get the publishing channel")

val thread = channel.asChannelOf<TextChannelThread>()
val firstMessage = thread.getFirstMessage()!!

val webhook = ensureWebhook(publishingChannel, "Quilt Devlogs") {
HttpClient().get(TOOLCHAIN_LOGO).body()
}

val match = ICON_URL_REGEX.find(firstMessage.content)

val icon = if (match != null) {
match.groupValues[1]
} else {
TOOLCHAIN_LOGO
}.trim()

val message = webhook.execute(webhook.token!!) {
this.username = thread.name
this.avatarUrl = icon
this.content = this@publishDevlog.content
}

if (publishingChannel is NewsChannel) {
message.publish()
}

addReaction("🚀")
}

inner class ForumChannelArgs : Arguments() {
inner class PostTagArgs : Arguments() {
override val parseForAutocomplete: Boolean = true

val post by channel {
name = "post"
description = "Thread to edit the tags for"

requireChannelType(ChannelType.PublicGuildThread)
}

val tag by tag {
name = "tag"
description = "Tag to create the post with"

channelGetter = { post.asChannelOfOrNull<TextChannelThread>()?.parent?.asChannelOfOrNull() }
}
}

inner class CreatePostArgs : Arguments() {
override val parseForAutocomplete: Boolean = true

val channel by channel {
name = "channel"
description = "Forum channel to post in"

requireChannelType(ChannelType.GuildForum)
}

val tag by optionalTag {
name = "tag"
description = "Tag to create the post with"

channelGetter = { channel.asChannelOfOrNull() }
}
}

inner class ForumPostArgs : Arguments() {
inner class EditPostArgs : Arguments() {
val post by optionalChannel {
name = "post"
description = "Thread to edit the first post for"
Expand Down
Loading

0 comments on commit 406473a

Please sign in to comment.