Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion src/main/kotlin/at/hannibal2/skyhanni/discord/BotConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ data class BotConfig(
val githubTokenOwn: String,
val githubTokenPullRequests: String,
val editPermissionRoleIds: Map<String, String>,
val githubWebhookUserId: String,
val openPrTagId: String
)

object ConfigLoader {
Expand All @@ -24,7 +26,9 @@ object ConfigLoader {
"TODO: github token with sh mod repo access",
mapOf(
"user friendly (non important) name" to "TODO: role id"
)
),
"TODO: github webhook 'user' id",
"TODO: discord forum open pull request tag id"
)
fun load(filePath: String): BotConfig {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import at.hannibal2.skyhanni.discord.Utils.logAction
import at.hannibal2.skyhanni.discord.Utils.reply
import at.hannibal2.skyhanni.discord.command.*
import at.hannibal2.skyhanni.discord.utils.ErrorManager.handleError
import net.dv8tion.jda.api.entities.channel.ChannelType
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import org.reflections.Reflections
import java.lang.reflect.Modifier
Expand Down Expand Up @@ -38,6 +39,9 @@ object CommandListener {
if (this.author.id == BOT_ID) {
BotMessageHandler.handle(this)
}
if (this.author.id == BOT.config.githubWebhookUserId) {
LinkListener.onMessage(bot, this)
}
return
}

Expand Down Expand Up @@ -66,7 +70,7 @@ object CommandListener {
return
}

if (!command.userCommand) {
if (!command.userCommand && channelType != ChannelType.GUILD_PUBLIC_THREAD) {
if (!hasAdminPermissions()) {
reply("No permissions $PLEADING_FACE")
return
Expand All @@ -78,7 +82,7 @@ object CommandListener {
}
}

// allows to use `!<command> -help` instaed of `!help -<command>`
// allows to use `!<command> -help` instead of `!help -<command>`
if (args.size == 1 && args.first() == "-help") {
with(HelpCommand) {
sendUsageReply(literal)
Expand Down
48 changes: 48 additions & 0 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ data class Tag(val keyword: String, var response: String, var uses: Int)
object Database {
private val connection: Connection = DriverManager.getConnection("jdbc:sqlite:bot.db")
private val tags = mutableMapOf<String, Tag>()
private val linkedForumPosts = mutableMapOf<String, Int>() // key = channel id, value = pr number

init {
val statement = connection.createStatement()
Expand All @@ -19,7 +20,16 @@ object Database {
append("response TEXT, ")
append("count INTEGER DEFAULT 0)")
})

statement.execute(buildString {
append("CREATE TABLE IF NOT EXISTS linked_posts (")
append("id INTEGER PRIMARY KEY AUTOINCREMENT, ")
append("channel_id STRING UNIQUE, ")
append("pull_request_id INTEGER UNIQUE)")
})

loadTagCache()
loadLinkCache()
}

private fun loadTagCache() {
Expand All @@ -34,6 +44,17 @@ object Database {
resultSet.close()
}

private fun loadLinkCache() {
val statement = connection.prepareStatement("SELECT channel_id, pull_request_id FROM linked_posts")
val resultSet = statement.executeQuery()
while (resultSet.next()) {
val channelId = resultSet.getString("channel_id")
val pr = resultSet.getInt("pull_request_id")
linkedForumPosts[channelId] = pr
}
resultSet.close()
}

private fun ensureCountColumnExists() {
val statement = connection.prepareStatement("PRAGMA table_info(keywords)")
val resultSet = statement.executeQuery()
Expand Down Expand Up @@ -66,6 +87,19 @@ object Database {
return updated
}

fun addLink(channelId: String, pr: Int): Boolean {
val statement = connection.prepareStatement(
"INSERT OR REPLACE INTO linked_posts (channel_id, pull_request_id) VALUES (?, ?)"
)
statement.setString(1, channelId)
statement.setInt(2, pr)
val updated = statement.executeUpdate() > 0
if (updated) {
linkedForumPosts[channelId] = pr
}
return updated
}

fun getResponse(keyword: String, increment: Boolean = false): String? {
val key = keyword.lowercase()
val kObj = tags[key] ?: return null
Expand All @@ -81,6 +115,10 @@ object Database {
return kObj.response
}

fun getChannelId(prNumber: Int): String? = linkedForumPosts.entries.find { it.value == prNumber }?.key

fun getPullrequest(channelId: String): Int? = linkedForumPosts[channelId]

fun deleteTag(keyword: String): Boolean {
val key = keyword.lowercase()
val statement = connection.prepareStatement("DELETE FROM keywords WHERE keyword = ?")
Expand All @@ -90,8 +128,18 @@ object Database {
return updated
}

fun deleteLink(channelId: String): Boolean {
val statement = connection.prepareStatement("DELETE FROM linked_posts WHERE channel_id = ?")
statement.setString(1, channelId)
val updated = statement.executeUpdate() > 0
if (updated) linkedForumPosts.remove(channelId)
return updated
}

fun listTags(): List<Tag> = tags.values.toList()

fun listLinks(): Map<String, Int> = linkedForumPosts

fun getTagCount(keyword: String): Int? {
return tags[keyword.lowercase()]?.uses
}
Expand Down
33 changes: 33 additions & 0 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/LinkListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package at.hannibal2.skyhanni.discord

import at.hannibal2.skyhanni.discord.Utils.messageSend
import net.dv8tion.jda.api.events.message.MessageReceivedEvent

object LinkListener {

private val githubPattern =
"\\[SkyHanniStudios/DiscordBot] (New comment on pull request|Pull request review submitted:) #(?<pr>\\d+):? .+".toPattern()

fun onMessage(bot: DiscordBot, event: MessageReceivedEvent) {
event.onMessage(bot)
}

private fun MessageReceivedEvent.onMessage(bot: DiscordBot) {
val embed = this.message.embeds[0]
val title = embed.title ?: return

val prNumber = getPr(title)?.toInt() ?: return
val channelId = Database.getChannelId(prNumber) ?: return

val guild = bot.jda.getGuildById(BOT.config.allowedServerId) ?: return
val channel = guild.getThreadChannelById(channelId) ?: return

channel.messageSend(this.message.embeds[0])
}

private fun getPr(title: String): String? {
val matcher = githubPattern.matcher(title)
if (!matcher.matches()) return null
return matcher.group("pr")
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ object Utils {
logAction("Error: $text")
}

fun MessageReceivedEvent.userSuccess(text: String) {
message.messageReply("✅ $text")
}

fun MessageReceivedEvent.reply(embed: MessageEmbed) {
message.messageReply(embed)
}
Expand Down Expand Up @@ -81,6 +85,14 @@ object Utils {
}
}

fun MessageChannel.messageSend(embed: MessageEmbed, instantly: Boolean = false) {
if (instantly) {
sendMessageEmbeds(embed).complete()
} else {
sendMessageEmbeds(embed).queue()
}
}

fun Message.replyWithConsumer(text: String, consumer: (MessageReceivedEvent) -> Unit) {
BotMessageHandler.log(text, consumer)
messageReply(text)
Expand Down
129 changes: 129 additions & 0 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/command/LinkCommands.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package at.hannibal2.skyhanni.discord.command

import at.hannibal2.skyhanni.discord.BOT
import at.hannibal2.skyhanni.discord.Database
import at.hannibal2.skyhanni.discord.Option
import at.hannibal2.skyhanni.discord.PLEADING_FACE
import at.hannibal2.skyhanni.discord.Utils.logAction
import at.hannibal2.skyhanni.discord.Utils.reply
import at.hannibal2.skyhanni.discord.Utils.userError
import at.hannibal2.skyhanni.discord.Utils.userSuccess
import at.hannibal2.skyhanni.discord.command.LinkCommand.setTags
import at.hannibal2.skyhanni.discord.command.LinkCommand.setTitle
import at.hannibal2.skyhanni.discord.command.PullRequestCommand.parseValidPrNumber
import at.hannibal2.skyhanni.discord.github.GitHubClient
import net.dv8tion.jda.api.entities.channel.ChannelType
import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel
import net.dv8tion.jda.api.entities.channel.forums.ForumTag
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.managers.channel.concrete.ThreadChannelManager

object LinkCommand : BaseCommand() {
override val name = "link"

override val description = "Link a forum post to a pull request."
override val options: List<Option> = listOf(
Option("number", "Number of the pull request you want the post to be linked to.")
)

override fun MessageReceivedEvent.execute(args: List<String>) {
if (args.size != 1) return wrongUsage("<number>")
val prNumber = parseValidPrNumber(args.first()) ?: return

if (!isValidPrNumber(prNumber)) return

val post = channel.asThreadChannel()
val manager = post.manager

Database.getPullrequest(channel.id)?.let {
reply("Post already linked to $it $PLEADING_FACE")
return
}

Database.addLink(post.id, prNumber)
logAction("linked pr #$prNumber to this channel")

val titleFormat = prNumber.titleFormat()
if (!post.name.contains(titleFormat)) {
manager.setTitle("${post.name}$titleFormat")
}

post.updateTags(true)

userSuccess("Successfully linked PR $prNumber to this post.")
}

fun ThreadChannelManager.setTags(tags: List<ForumTag>) {
setAppliedTags(tags).queue()
}

fun ThreadChannelManager.setTitle(name: String) {
setName(name).queue()
}

private const val USER = "hannibal002"
private const val REPO = "SkyHanni"
private val github = GitHubClient(USER, REPO, BOT.config.githubTokenPullRequests)

private fun MessageReceivedEvent.isValidPrNumber(number: Int): Boolean {
if (!isFromType(ChannelType.GUILD_PUBLIC_THREAD)) {
userError("Wrong channel $PLEADING_FACE")
return false
}

try {
github.findPullRequest(number) ?: run {
userError("Pull request with number $number not found")
return false
}
} catch (e: Exception) {
userError("Pull request with number $number not found")
return false
}
return true
}
}

private fun ThreadChannel.updateTags(add: Boolean) {
val tags = appliedTags
val tag = parentChannel.asForumChannel().getAvailableTagById(BOT.config.openPrTagId) ?: return
if (add && tag !in tags) tags.add(tag) else if (!add && tag in tags) tags.remove(tag)

manager.setTags(tags)
}

private fun Int.titleFormat() = " (PR #$this)"

object UnlinkCommand : BaseCommand() {
override val name = "unlink"

override val description = "Unlink a forum post from a pull request."

override fun MessageReceivedEvent.execute(args: List<String>) {
if (!isFromType(ChannelType.GUILD_PUBLIC_THREAD)) {
userError("Wrong channel $PLEADING_FACE")
return
}

val pr = Database.getPullrequest(channel.id) ?: run {
userError("Post isn't linked to any pull request $PLEADING_FACE")
return
}

val post = channel.asThreadChannel()
val manager = post.manager

Database.deleteLink(post.id)
logAction("unlinked pr #$pr from this channel")

val titleFormat = pr.titleFormat()
if (post.name.contains(titleFormat)) {
manager.setTitle(post.name.replace(titleFormat, ""))
}

post.updateTags(false)

userSuccess("Successfully unlinked this post.")
}
}
Loading