QuiltMC / cozy-discord

Discord bot used for Quilt's day-to-day tasks. Discord avatar based on Feuerfuchs blobfox emoji at https://www.feuerfuchs.dev/en/projects/blobfox-emojis/
Mozilla Public License 2.0
23 stars 24 forks source link
cozy discord discord-bot fox kordex kotlin

Cozy: Discord

This repository contains a Discord bot that we make use of to help keep the Quilt community servers running smoothly. Its features include, but are not limited to:

Most of the features currently implemented within Cozy were designed with Quilt in mind, and haven't been factored out into reusable modules. We do plan to do this at some point, but there's a ways to go yet!

Functionality is split into several modes:

Modes are specified via the MODE environment variable - see below for more information on that.

Development Requirements

If you're here to help out, here's what you'll need. Firstly:

Setting Up

As a first step, fork this repository, clone your fork, and open it in your IDE, importing the Gradle project. Create a file named .env in the project root (next to files like the build.gradle.kts), and fill it out with your bot's settings. This file should contain KEY=value pairs, without a space around the = and without added quotes:

TOKEN=AAA....
DB_URL=mongodb://localhost:27017/

ENVIRONMENT=dev
# You get the idea.

Required settings:

Logging settings:

Settings used by all modes:

Settings used by mode: quilt

Settings used by mode: dev

Once you've filled out your .env file, you can use the run gradle task to launch the bot. If this is your first run, you'll want to start with the quilt mode as this is the mode that runs the database migrations. After that, feel free to set up and test whichever mode you need to work with.

Conventions and Linting

This repository makes use of detekt, a static analysis tool for Kotlin code. Our formatting rules are contained within detekt.yml, but detekt can't verify everything.

To be specific, proper spacing is important for code readability. If your code is too dense, then we're going to ask you to fix this problem - so try to bear it in mind. Let's see some examples...

Bad

override suspend fun unload() {
    super.unload()
    if (::task.isInitialized) {
        task.cancel()
    }
}
action {
    val channel = channel.asChannel() as ThreadChannel
    val member = user.asMember(guild!!.id)
    val roles = member.roles.toList().map { it.id }
    if (MODERATOR_ROLES.any { it in roles }) {
        targetMessages.forEach { it.pin("Pinned by ${member.tag}") }
        edit { content = "Messages pinned." }
        return@action
    }
    if (channel.ownerId != user.id && threads.isOwner(channel, user) != true) {
        respond { content = "**Error:** This is not your thread." }
        return@action
    }
    targetMessages.forEach { it.pin("Pinned by ${member.tag}") }
    edit { content = "Messages pinned." }
}
action {
    if (this.member?.asMemberOrNull()?.mayManageRole(arguments.role) == true) {
        arguments.targetUser.removeRole(
            arguments.role.id,
            "${this.user.asUserOrNull()?.tag ?: this.user.id} used /team remove"
        )
        respond {
            content = "Successfully removed ${arguments.targetUser.mention} from " +
                    "${arguments.role.mention}."
            allowedMentions { }
        }
    } else {
        respond {
            content = "Your team needs to be above ${arguments.role.mention} in order to remove " +
                    "anyone from it."
            allowedMentions { }
        }
    }
}

Good

override suspend fun unload() {
    super.unload()

    if (::task.isInitialized) {
        task.cancel()
    }
}
action {
    val channel = channel.asChannel() as ThreadChannel
    val member = user.asMember(guild!!.id)
    val roles = member.roles.toList().map { it.id }

    if (MODERATOR_ROLES.any { it in roles }) {
        targetMessages.forEach { it.pin("Pinned by ${member.tag}") }
        edit { content = "Messages pinned." }

        return@action
    }

    if (channel.ownerId != user.id && threads.isOwner(channel, user) != true) {
        respond { content = "**Error:** This is not your thread." }

        return@action
    }

    targetMessages.forEach { it.pin("Pinned by ${member.tag}") }

    edit { content = "Messages pinned." }
}
action {
    if (this.member?.asMemberOrNull()?.mayManageRole(arguments.role) == true) {
        arguments.targetUser.removeRole(
            arguments.role.id,

            "${this.user.asUserOrNull()?.tag ?: this.user.id} used /team remove"
        )

        respond {
            content = "Successfully removed ${arguments.targetUser.mention} from " +
                    "${arguments.role.mention}."

            allowedMentions { }
        }
    } else {
        respond {
            content = "Your team needs to be above ${arguments.role.mention} in order to remove " +
                    "anyone from it."

            allowedMentions { }
        }
    }
}

Hopefully these examples help to make things clearer. Group similar types of statements together (variable assignments), separating them from other types (like function calls). If a statement takes up multiple lines, then it probably needs to be separated from any other statements. In general, use your best judgement - extra space is better than not enough space, and detekt will tell you if you go overboard.