diff --git a/build.gradle.kts b/build.gradle.kts index 31adcb4..c546ad8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { implementation(group = "ch.qos.logback", name = "logback-classic", version = "1.2.3") - implementation(group = "com.sparkjava", name = "spark-core", version = "2.9.2") + implementation(group = "com.sparkjava", name = "spark-core", version = "2.9.3") implementation(group = "org.apache.velocity", name = "velocity-engine-core", version = "2.2") implementation(group = "com.github.ben-manes.caffeine", name = "caffeine", version = "2.8.5") @@ -35,6 +35,16 @@ dependencies { implementation(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = "2.10.1") implementation(group = "net.sf.trove4j", name = "trove4j", version = "3.0.3") + + implementation(group = "com.discord4j", name = "discord4j-core", version = "3.1.6") + + // TODO: remove after switch to d4j + implementation(group = "net.dv8tion", name = "JDA", version = "4.2.1_264") { + exclude(module = "opus-java") + } + + // TODO: remove after switch to d4j + // TODO: custom oauth client? // implementation(group = "com.jagrosh", name = "jda-utilities-oauth2", version = "3.0.5") implementation(group = "com.github.JDA-Applications", name = "JDA-Utilities", version = "804d58a") { // This is fine @@ -43,16 +53,6 @@ dependencies { exclude(module = "jda-utilities-command") exclude(module = "jda-utilities-menu") } - implementation(group = "net.dv8tion", name = "JDA", version = "4.3.0_298") { - exclude(module = "opus-java") - } - - // Yes, this is JDA - // We're running this PR https://github.com/DV8FromTheWorld/JDA/pull/1178 - // but it is broken atm - /*implementation(group = "com.github.dv8fromtheworld", name = "JDA", version = "68f4c4b") { - exclude(module = "opus-java") - }*/ } configure { diff --git a/src/main/kotlin/com/dunctebot/dashboard/Container.kt b/src/main/kotlin/com/dunctebot/dashboard/Container.kt index 1ea64d6..1ddfad8 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/Container.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/Container.kt @@ -2,11 +2,11 @@ package com.dunctebot.dashboard import com.dunctebot.dashboard.websocket.WebsocketClient import com.dunctebot.duncteapi.DuncteApi -import com.dunctebot.jda.JDARestClient +import com.dunctebot.discord.DiscordRestClient import com.fasterxml.jackson.databind.json.JsonMapper import okhttp3.OkHttpClient -val restJDA = JDARestClient(System.getenv("BOT_TOKEN")) +val discordClient = DiscordRestClient(System.getenv("BOT_TOKEN")) val duncteApis = DuncteApi("Bot ${System.getenv("BOT_TOKEN")}") val httpClient = OkHttpClient() diff --git a/src/main/kotlin/com/dunctebot/dashboard/WebHelpers.kt b/src/main/kotlin/com/dunctebot/dashboard/WebHelpers.kt index 17394f2..1141c13 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/WebHelpers.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/WebHelpers.kt @@ -8,8 +8,8 @@ import com.dunctebot.dashboard.rendering.WebVariables import com.fasterxml.jackson.databind.JsonNode import com.jagrosh.jdautilities.oauth2.OAuth2Client import com.jagrosh.jdautilities.oauth2.session.Session -import net.dv8tion.jda.api.entities.Guild -import net.dv8tion.jda.internal.utils.IOUtil +import discord4j.discordjson.json.GuildUpdateData +import discord4j.rest.entity.RestGuild import okhttp3.FormBody import spark.* import java.net.URLDecoder @@ -43,11 +43,18 @@ val Request.userId: String val Request.guildId: String? get() = this.params(GUILD_ID) -fun Request.fetchGuild(): Guild? { - val guildId: String = this.guildId ?: return null +val Request.guild: RestGuild? + get() { + val guildId = this.guildId ?: return null + + return discordClient.getGuild(guildId.toLong()) + } + +fun Request.fetchGuild(): GuildUpdateData? { + val guild: RestGuild = this.guild ?: return null return try { - restJDA.retrieveGuildById(guildId).complete() + guild.data.block() } catch (e: Exception) { e.printStackTrace() null @@ -127,8 +134,9 @@ fun verifyCaptcha(response: String): JsonNode { .post(body) .build() ).execute().use { - val readFully = IOUtil.readFully(IOUtil.getBody(it)) - - return jsonMapper.readTree(readFully) + it.body().use { body -> + // reads the entire body into memory + return jsonMapper.readTree(body!!.bytes()) + } } } diff --git a/src/main/kotlin/com/dunctebot/dashboard/WebServer.kt b/src/main/kotlin/com/dunctebot/dashboard/WebServer.kt index 663aba8..3e6b1c3 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/WebServer.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/WebServer.kt @@ -9,17 +9,17 @@ import com.dunctebot.dashboard.controllers.api.DataController import com.dunctebot.dashboard.controllers.api.GuildApiController import com.dunctebot.dashboard.controllers.api.OtherAPi import com.dunctebot.dashboard.controllers.errors.HttpErrorHandlers -import com.dunctebot.dashboard.rendering.VelocityRenderer import com.dunctebot.dashboard.rendering.WebVariables import com.dunctebot.dashboard.utils.fetchGuildPatronStatus +import com.dunctebot.dashboard.utils.getEffectivePermissions import com.dunctebot.models.settings.GuildSetting import com.dunctebot.models.settings.ProfanityFilterType import com.dunctebot.models.settings.WarnAction import com.dunctebot.models.utils.Utils -import com.fasterxml.jackson.databind.JsonNode import com.jagrosh.jdautilities.oauth2.OAuth2Client -import net.dv8tion.jda.api.entities.TextChannel -import spark.ModelAndView +import discord4j.common.util.Snowflake +import discord4j.rest.util.Permission +import discord4j.rest.util.PermissionSet import spark.Spark.* // The socket server will be used to communicate with DuncteBot himself @@ -202,11 +202,6 @@ class WebServer { return@get OtherAPi.uptimeRobot() } - // keep? - get("/commands.json") { _, _ -> - "TODO: setup websocket to bot" - } - post("/update-data") { request, _ -> return@post DataController.updateData(request) } @@ -255,19 +250,41 @@ class WebServer { private fun getWithGuildData(path: String, map: WebVariables, view: String) { get(path) { request, _ -> - val guild = request.fetchGuild() + val guild = request.guild if (guild != null) { - val guildId = guild.idLong + val guildId = guild.id.asLong() + val self = guild.selfMember.block()!! + val selfId = Snowflake.of(self.user().id()) + + val tcs = guild.channels + .filter { + it.getEffectivePermissions(guild, self).map { p -> + println("Permissions $p") + p.containsAll(PermissionSet.of( + Permission.SEND_MESSAGES, Permission.VIEW_CHANNEL /* read messages */ + )) + }.block()!! + } + .collectList() + .block()!! + + println("channels $tcs") + + val goodRoles = guild.roles + .filter { !it.managed() } + .filter { it.name() != "@everyone" && it.name() != "@here" } + // TODO: check if can interact + .collectList() + .block()!! - val tcs = guild.textChannelCache.filter(TextChannel::canTalk).toList() - val goodRoles = guild.roleCache.filter { + /*val goodRoles_old = guild.roleCache.filter { guild.selfMember.canInteract(it) && it.name != "@everyone" && it.name != "@here" - }.filter { !it.isManaged }.toList() + }.filter { !it.isManaged }.toList()*/ map.put("goodChannels", tcs) map.put("goodRoles", goodRoles) - map.put("guild", guild) + map.put("guild", discordClient.retrieveGuildData(guildId)) val settings = duncteApis.getGuildSetting(guildId) diff --git a/src/main/kotlin/com/dunctebot/dashboard/controllers/DashboardController.kt b/src/main/kotlin/com/dunctebot/dashboard/controllers/DashboardController.kt index 0464964..ecc6ec9 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/controllers/DashboardController.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/controllers/DashboardController.kt @@ -4,7 +4,8 @@ import com.dunctebot.dashboard.* import com.dunctebot.dashboard.WebServer.Companion.OLD_PAGE import com.dunctebot.dashboard.WebServer.Companion.SESSION_ID import com.dunctebot.dashboard.WebServer.Companion.USER_ID -import net.dv8tion.jda.api.Permission +import com.dunctebot.discord.extensions.hasPermission +import discord4j.rest.util.Permission import spark.Request import spark.Response import spark.Spark @@ -21,15 +22,16 @@ object DashboardController { } val guild = request.fetchGuild() ?: throw haltDiscordError(DiscordError.NO_GUILD, request.guildId!!) + val guildId = guild.id().asLong() val member = try { - restJDA.retrieveMemberById(guild, request.userId).complete() + discordClient.retrieveMemberById(guildId, request.userId).block()!! } catch (e: Exception) { e.printStackTrace() throw haltDiscordError(DiscordError.WAT) } - if (!member.hasPermission(Permission.MANAGE_SERVER)) { + if (!member.hasPermission(guildId, Permission.MANAGE_GUILD)) { throw haltDiscordError(DiscordError.NO_PERMS) } } diff --git a/src/main/kotlin/com/dunctebot/dashboard/controllers/GuildController.kt b/src/main/kotlin/com/dunctebot/dashboard/controllers/GuildController.kt index 83ff359..bc8a178 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/controllers/GuildController.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/controllers/GuildController.kt @@ -4,23 +4,24 @@ import com.dunctebot.dashboard.* import com.dunctebot.dashboard.rendering.DbModelAndView import com.dunctebot.dashboard.rendering.WebVariables import com.github.benmanes.caffeine.cache.Caffeine -import net.dv8tion.jda.api.entities.Member -import net.dv8tion.jda.api.entities.Role -import net.dv8tion.jda.api.exceptions.ErrorResponseException +import discord4j.discordjson.json.MemberData +import discord4j.discordjson.json.RoleData import spark.Request import spark.Response import java.util.concurrent.TimeUnit -import kotlin.streams.toList object GuildController { + private const val DEFAULT_ROLE_COLOUR = 0x1FFFFFFF + // some hash -> "$userId-$guildId" + // TODO: convert to expiring map val securityKeys = mutableMapOf() val guildHashes = Caffeine.newBuilder() .expireAfterWrite(2, TimeUnit.HOURS) .build() val guildRoleCache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) - .build>() + .build>>() fun handleOneGuildRegister(request: Request): Any { val params = request.paramsMap @@ -87,33 +88,35 @@ object GuildController { fun showGuildRoles(request: Request, response: Response): Any { val hash = request.params("hash") - val guildId = guildHashes.getIfPresent(hash) ?: return haltNotFound(request, response) - val guild = try { - // TODO: do we want to do this? - // Maybe only cache for a short time as it will get outdated data - restJDA.fakeJDA.getGuildById(guildId) ?: restJDA.retrieveGuildById(guildId.toString()).complete() - } catch (e: ErrorResponseException) { - e.printStackTrace() - return haltNotFound(request, response) - } + val guildId = guildHashes.getIfPresent(hash) ?: throw haltNotFound(request, response) +// val guildId = hash.toLong() // cheat :D - val roles = guildRoleCache.get(guild.idLong) { - val members = restJDA.retrieveAllMembers(guild).stream().toList() + val (guildName, roles) = guildRoleCache.get(guildId) { + val internalRoles = discordClient.retrieveGuildRoles(guildId) + val members = discordClient.retrieveGuildMembers(guildId).collectList().block()!! + val guild = discordClient.retrieveGuildData(guildId) - guild.roles.map { CustomRole(it, members) } + guild.name() to internalRoles.map { CustomRole(it, members) } + .sort { o1, o2 -> o2.position() - o1.position() } + .collectList().block()!! }!! return WebVariables() .put("hide_menu", true) - .put("title", "Roles for ${guild.name}") - .put("guild_name", guild.name) + .put("title", "Roles for $guildName") + .put("guild_name", guildName) .put("roles", roles) .toModelAndView("guildRoles.vm") } - class CustomRole(private val realRole: Role, allMembers: List) : Role by realRole { - // Accessed by our templating engine - @Suppress("unused") - val memberCount = allMembers.filter { it.roles.contains(realRole) }.size + // Accessed by our templating engine + @Suppress("unused") + class CustomRole(private val realRole: RoleData, allMembers: List) : RoleData by realRole { + val memberCount = allMembers.filter { it.roles().contains(realRole.id()) }.size + + // overloads mimicking JDA names for easy access in the template + val idLong = realRole.id().asLong() + val name: String = realRole.name() + val colorRaw = if (realRole.color() == 0) DEFAULT_ROLE_COLOUR else realRole.color() } } diff --git a/src/main/kotlin/com/dunctebot/dashboard/controllers/RootController.kt b/src/main/kotlin/com/dunctebot/dashboard/controllers/RootController.kt index 8023b74..04b3349 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/controllers/RootController.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/controllers/RootController.kt @@ -8,7 +8,6 @@ import com.jagrosh.jdautilities.oauth2.OAuth2Client import com.jagrosh.jdautilities.oauth2.OAuth2Client.DISCORD_REST_VERSION import com.jagrosh.jdautilities.oauth2.Scope import com.jagrosh.jdautilities.oauth2.exceptions.InvalidStateException -import net.dv8tion.jda.api.exceptions.HttpException import org.slf4j.LoggerFactory import spark.Request import spark.Response diff --git a/src/main/kotlin/com/dunctebot/dashboard/controllers/api/GuildApiController.kt b/src/main/kotlin/com/dunctebot/dashboard/controllers/api/GuildApiController.kt index ece83aa..6600e0a 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/controllers/api/GuildApiController.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/controllers/api/GuildApiController.kt @@ -3,9 +3,10 @@ package com.dunctebot.dashboard.controllers.api import com.dunctebot.dashboard.* import com.dunctebot.dashboard.controllers.GuildController import com.dunctebot.dashboard.utils.HashUtils +import com.dunctebot.discord.extensions.asTag import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode -import net.dv8tion.jda.api.entities.User +import discord4j.discordjson.json.UserData import spark.Request import spark.Response import java.util.concurrent.CompletableFuture @@ -45,8 +46,8 @@ object GuildApiController { .put("code", response.status()) } - val user: User? = try { - restJDA.retrieveUserById(data["user_id"].asText()).complete() + val user: UserData? = try { + discordClient.getUser(data["user_id"].asText()).data.block() } catch (e: Exception) { e.printStackTrace() null @@ -101,12 +102,13 @@ object GuildApiController { .put("id", guildId) .put("name", guild["name"].asText()) + val userId = user.id().asString() val userJson = jsonMapper.createObjectNode() - .put("id", user.id) - .put("name", user.name) + .put("id", userId) + .put("name", user.username()) .put("formatted", user.asTag) - val theKey = "${user.idLong}-${guildId}" + val theKey = "$userId-$guildId" val theHash = HashUtils.sha1(theKey + System.currentTimeMillis()) GuildController.securityKeys[theHash] = theKey diff --git a/src/main/kotlin/com/dunctebot/dashboard/utils/PermissionUtils.kt b/src/main/kotlin/com/dunctebot/dashboard/utils/PermissionUtils.kt new file mode 100644 index 0000000..6f667c5 --- /dev/null +++ b/src/main/kotlin/com/dunctebot/dashboard/utils/PermissionUtils.kt @@ -0,0 +1,88 @@ +package com.dunctebot.dashboard.utils + +import discord4j.common.util.Snowflake +import discord4j.core.`object`.PermissionOverwrite +import discord4j.core.util.PermissionUtil +import discord4j.discordjson.Id +import discord4j.discordjson.json.ChannelData +import discord4j.discordjson.json.MemberData +import discord4j.discordjson.json.RoleData +import discord4j.rest.entity.RestGuild +import discord4j.rest.util.PermissionSet +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.* +import java.util.function.Predicate +import java.util.stream.Collectors +import java.util.stream.Stream + +fun RestGuild.getEveryoneRole(): Mono { + return this.role(this.id).data +} + +val Id.snowflake: Snowflake + get() = Snowflake.of(this) + +/** + * Adapted from https://github.com/Discord4J/Discord4J/blob/0e29720384ce5da312ab81994ac89c9607e75d96/core/src/main/java/discord4j/core/object/entity/Member.java#L442 + */ +fun MemberData.getBasePermissions(guild: RestGuild): Mono { + val getIsOwner = guild.data.map { it.ownerId() == this.user().id() } + val getEveryonePerms = guild.getEveryoneRole().map { PermissionSet.of(it.permissions()) } + val getRolePerms = Flux.fromIterable(this.roles()) + .flatMap { guild.role(Snowflake.of(it)).data } + .map { PermissionSet.of(it.permissions()) } + .collectList() + + return getIsOwner.filter(Predicate.isEqual(true)) + .flatMap { Mono.just(PermissionSet.all()) } + .switchIfEmpty(Mono.zip(getEveryonePerms, getRolePerms, PermissionUtil::computeBasePermissions)) +} + +fun ChannelData.getPermissionOverwrites(): Set { + return this.permissionOverwrites().toOptional() + .map { permissionOverwrites -> + return@map permissionOverwrites.stream() + .map { data -> + when (data.type()) { + "role" -> PermissionOverwrite.forRole( + data.id().snowflake, + PermissionSet.of(data.allow()), + PermissionSet.of(data.deny()) + ) + "member" -> PermissionOverwrite.forMember( + data.id().snowflake, + PermissionSet.of(data.allow()), + PermissionSet.of(data.deny()) + ) + else -> throw IllegalArgumentException("Unknown override type ${data.type()}") + } + } + .collect(Collectors.toSet()) + } + .orElse(Collections.emptySet()) +} + +private fun getOverridesForRole(roleId: Snowflake, all: Set): Optional { + return all.stream().filter { it.roleId.map(roleId::equals).orElse(false) }.findFirst() +} + +private fun getOverridesForMember(memberId: Snowflake, all: Set): Optional { + return all.stream().filter { it.memberId.map(memberId::equals).orElse(false) }.findFirst() +} + +fun ChannelData.getEffectivePermissions(guild: RestGuild, member: MemberData): Mono { + return member.getBasePermissions(guild).map { basePerms -> + val permissionOverwrites = this.getPermissionOverwrites() + val everyoneOverwrite = getOverridesForRole(guild.id, permissionOverwrites).orElse(null) + + val roleOverwrites = member.roles() + .stream() + .map { getOverridesForRole(it.snowflake, permissionOverwrites) } + .flatMap { it.map { item -> Stream.of(item) }.orElseGet { Stream.empty() } } + .toList() + val memberOverwrites = getOverridesForMember(member.user().id().snowflake, permissionOverwrites).orElse(null) + + return@map PermissionUtil.computePermissions(basePerms, everyoneOverwrite, roleOverwrites, memberOverwrites) + } +} diff --git a/src/main/kotlin/com/dunctebot/dashboard/websocket/WebsocketClient.kt b/src/main/kotlin/com/dunctebot/dashboard/websocket/WebsocketClient.kt index 2d6efdb..19e1b9f 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/websocket/WebsocketClient.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/websocket/WebsocketClient.kt @@ -13,9 +13,9 @@ import com.dunctebot.dashboard.websocket.handlers.base.SocketHandler import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import com.neovisionaries.ws.client.* -import net.dv8tion.jda.internal.utils.IOUtil import org.slf4j.LoggerFactory import java.io.IOException +import java.net.URI import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -37,7 +37,7 @@ class WebsocketClient : WebSocketAdapter(), WebSocketListener { private val factory = WebSocketFactory() .setConnectionTimeout(5000) - .setServerName(IOUtil.getHost(System.getenv("WS_URL"))) + .setServerName(URI.create(System.getenv("WS_URL")).host) lateinit var socket: WebSocket var mayReconnect = true diff --git a/src/main/kotlin/com/dunctebot/dashboard/websocket/handlers/DataUpdateHandler.kt b/src/main/kotlin/com/dunctebot/dashboard/websocket/handlers/DataUpdateHandler.kt index 09aecef..70c6de9 100644 --- a/src/main/kotlin/com/dunctebot/dashboard/websocket/handlers/DataUpdateHandler.kt +++ b/src/main/kotlin/com/dunctebot/dashboard/websocket/handlers/DataUpdateHandler.kt @@ -1,6 +1,6 @@ package com.dunctebot.dashboard.websocket.handlers -import com.dunctebot.dashboard.restJDA +import com.dunctebot.dashboard.discordClient import com.dunctebot.dashboard.websocket.handlers.base.SocketHandler import com.fasterxml.jackson.databind.JsonNode @@ -8,7 +8,7 @@ class DataUpdateHandler : SocketHandler() { override fun handleInternally(data: JsonNode?) { if (data!!.has("guilds")) { data["guilds"]["invalidate"].forEach { - restJDA.invalidateGuild(it.asLong()) + discordClient.invalidateGuild(it.asLong()) } } } diff --git a/src/main/kotlin/com/dunctebot/discord/DiscordRestClient.kt b/src/main/kotlin/com/dunctebot/discord/DiscordRestClient.kt new file mode 100644 index 0000000..5302f90 --- /dev/null +++ b/src/main/kotlin/com/dunctebot/discord/DiscordRestClient.kt @@ -0,0 +1,63 @@ +package com.dunctebot.discord + +import com.github.benmanes.caffeine.cache.Caffeine +import discord4j.common.util.Snowflake +import discord4j.core.DiscordClient +import discord4j.discordjson.json.* +import discord4j.rest.entity.RestGuild +import discord4j.rest.entity.RestUser +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.concurrent.TimeUnit + +/** + * Custom jda rest client that allows for rest-only usage of JDA + * + * This class has been inspired by GivawayBot and all credit goes to them https://github.com/jagrosh/GiveawayBot + */ +class DiscordRestClient(token: String) { + // create a guild cache that keeps the guilds in cache for 30 minutes + // When we stop accessing the guild it will be removed from the cache + // TODO: use expiring map instead + private val guildCache = Caffeine.newBuilder() + .expireAfterAccess(30, TimeUnit.MINUTES) + .build() + + private val client = DiscordClient.create(token) + + // TODO: still needed? + fun invalidateGuild(guildId: Long) { + this.guildCache.invalidate(guildId.toString()) + } + + // Note: instantly starts the request + fun getUser(id: String): RestUser { + return this.client.getUserById(Snowflake.of(id)) + } + + fun getGuild(guildId: Long): RestGuild { + return this.client.getGuildById(Snowflake.of(guildId)) + } + + fun retrieveGuildData(guildId: Long): GuildUpdateData { + return guildCache.get(guildId) { + this.getGuild(guildId).data.block()!! + }!! + } + + fun retrieveGuildRoles(guildId: Long): Flux { + return this.getGuild(guildId).roles + } + + fun retrieveGuildMembers(guildId: Long): Flux { + return this.getGuild(guildId).members + } + + fun retrieveMemberById(guildId: Long, memberId: String): Mono { + return this.getGuild(guildId).getMember(Snowflake.of(memberId)) + } + + fun retrieveRole(guildId: Long, roleId: Long): Mono { + return client.getRoleById(Snowflake.of(guildId), Snowflake.of(roleId)).data + } +} diff --git a/src/main/kotlin/com/dunctebot/discord/extensions/MemberData.kt b/src/main/kotlin/com/dunctebot/discord/extensions/MemberData.kt new file mode 100644 index 0000000..7156b79 --- /dev/null +++ b/src/main/kotlin/com/dunctebot/discord/extensions/MemberData.kt @@ -0,0 +1,63 @@ +package com.dunctebot.discord.extensions + +import com.dunctebot.dashboard.discordClient +import com.dunctebot.discord.isPermissionApplied +import discord4j.discordjson.json.MemberData +import discord4j.discordjson.json.RoleData +import discord4j.rest.util.Permission +import discord4j.rest.util.PermissionSet +import reactor.core.publisher.Flux + +fun MemberData.hasPermission(guildId: Long, perm: Permission): Boolean { + val permissions = this.fetchPermissions(guildId) + + return permissions.contains(Permission.ADMINISTRATOR) || permissions.contains(perm) +} + +// TODO: permission cache +fun MemberData.fetchPermissions(guildId: Long): PermissionSet { + val roles = this.roles() + + if (roles.isEmpty()) { + return PermissionSet.none() + } + + var perms = PermissionSet.none() + + roles.forEach { + val rolePerms = discordClient.retrieveRole(guildId, it.asLong()) + .block()!! + .permissions() + + // If the permission is not applied yet + perms = perms.or(PermissionSet.of(rolePerms)) + } + + return perms +} + +fun MemberData.hasPermission(guildRoles: Flux, perm: Permission): Boolean { + return isPermissionApplied(this.getPermissionsRaw(guildRoles), perm.value) +} + +fun MemberData.getPermissionsRaw(guildRoles: Flux): Long { + val roles = this.roles() + + if (roles.isEmpty()) { + return 0L + } + + var perms = 0L + + guildRoles.filter { roles.contains(it.id()) } + .map { it.permissions() } + .collectList().block()!! + .forEach { + // If the permission is not applied yet + if (!isPermissionApplied(perms, it)) { + perms = perms or it + } + } + + return perms +} diff --git a/src/main/kotlin/com/dunctebot/discord/extensions/UserData.kt b/src/main/kotlin/com/dunctebot/discord/extensions/UserData.kt new file mode 100644 index 0000000..a3a0844 --- /dev/null +++ b/src/main/kotlin/com/dunctebot/discord/extensions/UserData.kt @@ -0,0 +1,6 @@ +package com.dunctebot.discord.extensions + +import discord4j.discordjson.json.UserData + +val UserData.asTag: String + get() = "${this.username()}#${this.discriminator()}" diff --git a/src/main/kotlin/com/dunctebot/discord/utils.kt b/src/main/kotlin/com/dunctebot/discord/utils.kt new file mode 100644 index 0000000..16207d8 --- /dev/null +++ b/src/main/kotlin/com/dunctebot/discord/utils.kt @@ -0,0 +1,5 @@ +package com.dunctebot.discord + +fun isPermissionApplied(totalPermissions: Long, permToCheck: Long): Boolean { + return (totalPermissions and permToCheck) == permToCheck +} diff --git a/src/main/kotlin/com/dunctebot/jda/FakeJDA.kt b/src/main/kotlin/com/dunctebot/jda/FakeJDA.kt deleted file mode 100644 index c48c0ba..0000000 --- a/src/main/kotlin/com/dunctebot/jda/FakeJDA.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.dunctebot.jda - -import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.entities.User -import net.dv8tion.jda.api.requests.RestAction -import net.dv8tion.jda.internal.JDAImpl - -class FakeJDA(private val restClient: JDARestClient, fakeClient: JDAImpl) : JDA by fakeClient { - override fun retrieveUserById(id: Long, update: Boolean): RestAction { - return restClient.retrieveUserById(id.toString()) - } -} diff --git a/src/main/kotlin/com/dunctebot/jda/FakeSessionController.kt b/src/main/kotlin/com/dunctebot/jda/FakeSessionController.kt deleted file mode 100644 index 14e32d0..0000000 --- a/src/main/kotlin/com/dunctebot/jda/FakeSessionController.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.dunctebot.jda - -import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.utils.SessionController -import net.dv8tion.jda.internal.utils.tuple.Pair - -class FakeSessionController : SessionController { - private var globalRateLimit = 0L - - override fun appendSession(node: SessionController.SessionConnectNode) { - // do nothing - } - - override fun removeSession(node: SessionController.SessionConnectNode) { - TODO("Not yet implemented") - } - - override fun getGlobalRatelimit(): Long = globalRateLimit - - override fun setGlobalRatelimit(ratelimit: Long) { - globalRateLimit = ratelimit - } - - override fun getGateway(api: JDA): String { - TODO("Not yet implemented") - } - - override fun getGatewayBot(api: JDA): Pair { - TODO("Not yet implemented") - } -} diff --git a/src/main/kotlin/com/dunctebot/jda/JDARestClient.kt b/src/main/kotlin/com/dunctebot/jda/JDARestClient.kt deleted file mode 100644 index 8ea470c..0000000 --- a/src/main/kotlin/com/dunctebot/jda/JDARestClient.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.dunctebot.jda - -import com.dunctebot.jda.impl.MemberPaginationActionImpl -import com.github.benmanes.caffeine.cache.Caffeine -import net.dv8tion.jda.api.entities.Guild -import net.dv8tion.jda.api.entities.Member -import net.dv8tion.jda.api.entities.SelfUser -import net.dv8tion.jda.api.entities.User -import net.dv8tion.jda.api.requests.RestAction -import net.dv8tion.jda.api.utils.MiscUtil -import net.dv8tion.jda.api.utils.data.DataArray -import net.dv8tion.jda.internal.JDAImpl -import net.dv8tion.jda.internal.entities.GuildImpl -import net.dv8tion.jda.internal.requests.CompletedRestAction -import net.dv8tion.jda.internal.requests.RestActionImpl -import net.dv8tion.jda.internal.requests.Route -import net.dv8tion.jda.internal.utils.config.AuthorizationConfig -import net.dv8tion.jda.internal.utils.config.MetaConfig -import net.dv8tion.jda.internal.utils.config.SessionConfig -import net.dv8tion.jda.internal.utils.config.ThreadingConfig -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - - -/** - * Custom jda rest client that allows for rest-only usage of JDA - * - * This class has been inspired by GivawayBot and all credit goes to them https://github.com/jagrosh/GiveawayBot - */ -class JDARestClient(token: String) { - // create a guild cache that keeps the guilds in cache for 30 minutes - // When we stop accessing the guild it will be removed from the cache - private val guildCache = Caffeine.newBuilder() - .expireAfterAccess(30, TimeUnit.MINUTES) - .build() - - private val jda: JDAImpl - - init { - val authConfig = AuthorizationConfig(token) - val sessionConfig = SessionConfig.getDefault() - val threadConfig = ThreadingConfig.getDefault() - val metaConfig = MetaConfig.getDefault() - - threadConfig.setRateLimitPool(Executors.newScheduledThreadPool(5) { - val t = Thread(it, "dunctebot-rest-thread") - t.isDaemon = true - return@newScheduledThreadPool t - }, true) - - jda = JDAImpl(authConfig, sessionConfig, threadConfig, metaConfig) - - retrieveSelfUser().queue(jda::setSelfUser) - } - - // is public for JDA utils - val fakeJDA = FakeJDA(this, jda) - - fun invalidateGuild(guildId: Long) { - jda.guildsView.remove(guildId) - guildCache.invalidate(guildId.toString()) - } - - fun retrieveUserById(id: String): RestAction { - val route = Route.Users.GET_USER.compile(id) - - return RestActionImpl(jda, route) { - response, _ -> jda.entityBuilder.createUser(response.getObject()) - } - } - - private fun retrieveSelfUser(): RestAction { - val route = Route.Self.GET_SELF.compile() - - return RestActionImpl(jda, route) { - response, _ -> jda.entityBuilder.createSelfUser(response.getObject()) - } - } - - fun retrieveAllMembers(guild: Guild): MemberPaginationAction = MemberPaginationActionImpl(guild) - - private fun retrieveGuildChannelsArray(guildId: String): RestAction { - val route = Route.Guilds.GET_CHANNELS.compile(guildId) - - return RestActionImpl(jda, route) { response, _ -> response.array } - } - - fun retrieveMemberById(guild: Guild, memberId: String): RestAction { - val route = Route.Guilds.GET_MEMBER.compile(guild.id, memberId) - - return RestActionImpl(jda, route) { - response, _ -> jda.entityBuilder.createMember(guild as GuildImpl, response.getObject()) - } - } - - fun retrieveGuildById(id: String): RestAction { - // We're caching two events here, is that worth it? - /*// Lookup the guild from the cache - val guildById = jda.getGuildById(id) - - // If we already have it we will return the cached guild - // TODO: invalidation of caches - if (guildById != null) { - return CompletedRestAction(jda, guildById) - }*/ - - // Temp cache - val cachedGuild = guildCache.getIfPresent(id) - - if (cachedGuild != null && cachedGuild.channels.isNotEmpty()) { - return CompletedRestAction(jda, cachedGuild) - } - - val route = Route.Guilds.GET_GUILD.compile(id).withQueryParams("with_counts", "true") - - // if the first rest action fails the second one will never be called - return retrieveGuildChannelsArray(id).flatMap { channels -> - RestActionImpl(jda, route) { response, _ -> - val data = response.getObject() - - // fake a bit of data - data.put("channels", channels) - data.put("voice_states", DataArray.empty()) - - val guild = jda.entityBuilder - .createGuild(id.toLong(), data, MiscUtil.newLongMap(), data.getInt("approximate_member_count")) - - val selfMember = retrieveMemberById(guild, jda.selfUser.id).complete() - guild.membersView.writeLock().use { - guild.membersView.map.put(jda.selfUser.idLong, selfMember) - } - - guildCache.put(id, guild) - - guild - } - } - } -} diff --git a/src/main/kotlin/com/dunctebot/jda/MemberPaginationAction.kt b/src/main/kotlin/com/dunctebot/jda/MemberPaginationAction.kt deleted file mode 100644 index fa2e8f4..0000000 --- a/src/main/kotlin/com/dunctebot/jda/MemberPaginationAction.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.dunctebot.jda - -import net.dv8tion.jda.api.entities.Guild -import net.dv8tion.jda.api.entities.Member -import net.dv8tion.jda.api.requests.restaction.pagination.PaginationAction - -interface MemberPaginationAction : PaginationAction { - val guild: Guild -} diff --git a/src/main/kotlin/com/dunctebot/jda/impl/MemberPaginationActionImpl.kt b/src/main/kotlin/com/dunctebot/jda/impl/MemberPaginationActionImpl.kt deleted file mode 100644 index 36a2b90..0000000 --- a/src/main/kotlin/com/dunctebot/jda/impl/MemberPaginationActionImpl.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.dunctebot.jda.impl - -import com.dunctebot.jda.MemberPaginationAction -import net.dv8tion.jda.api.entities.Guild -import net.dv8tion.jda.api.entities.Member -import net.dv8tion.jda.api.exceptions.ParsingException -import net.dv8tion.jda.api.requests.Request -import net.dv8tion.jda.api.requests.Response -import net.dv8tion.jda.internal.entities.GuildImpl -import net.dv8tion.jda.internal.requests.Route -import net.dv8tion.jda.internal.requests.restaction.pagination.PaginationActionImpl - -class MemberPaginationActionImpl(override val guild: Guild) : - PaginationActionImpl( - guild.jda, - Route.get("guilds/${guild.id}/members").compile(), - 1, - 1000, - 1000 - ), MemberPaginationAction { - override fun getKey(it: Member): Long = it.idLong - - override fun finalizeRoute(): Route.CompiledRoute { - var route = super.finalizeRoute() - - var after: String? = null - val limit = getLimit().toString() - val last = this.lastKey - - if (last != 0L) { - after = last.toString() - } - - route = route.withQueryParams("limit", limit) - - if (after != null) { - route = route.withQueryParams("after", after) - } - - return route - } - - override fun handleSuccess(response: Response, request: Request>) { - val builder = api.entityBuilder - val array = response.array - val members = mutableListOf() - - for (i in 0 until array.length()) { - try { - val member = builder.createMember(guild as GuildImpl, array.getObject(i)) - - members.add(member) - - if (useCache) { - cached.add(member) - } - - last = member - lastKey = member.idLong - } catch (e: ParsingException) { - LOG.warn("Encountered exception in MemberPagination", e) - } catch (e: NullPointerException) { - LOG.warn("Encountered exception in MemberPagination", e) - } - } - - request.onSuccess(members) - } -} diff --git a/src/main/resources/views/dashboard/includes/basic/autoRole.vm b/src/main/resources/views/dashboard/includes/basic/autoRole.vm index 3aacf54..c3a9c36 100644 --- a/src/main/resources/views/dashboard/includes/basic/autoRole.vm +++ b/src/main/resources/views/dashboard/includes/basic/autoRole.vm @@ -3,10 +3,10 @@ #foreach($textChannel in $goodChannels) - #if($settings.getLogChannel() == $textChannel.getIdLong()) - + #if($settings.getLogChannel() == $textChannel.id().asLong()) + #else - + #end #end diff --git a/src/main/resources/views/errors/discord/noPerms.vm b/src/main/resources/views/errors/discord/noPerms.vm index 8f44aa9..bb179d1 100644 --- a/src/main/resources/views/errors/discord/noPerms.vm +++ b/src/main/resources/views/errors/discord/noPerms.vm @@ -1,13 +1,13 @@ #define ($content) -

- Missing permissions! -

+

+ Missing permissions! +

-

- You do not have access to edit the settings for this server, if you believe this is an error please contact your server administrator and ensure that you have the "Manage Server" permission -
- If you have this permission and are still seeing this message please wait 15 minutes and try again after. -

+

+ You do not have access to edit the settings for this server, if you believe this is an error please contact your server administrator and ensure that you have the "Manage Server" permission +
+ If you have this permission and are still seeing this message please wait 15 minutes and try again after. +

#end #parse('/views/templates/base.vm') diff --git a/src/main/resources/views/templates/base.vm b/src/main/resources/views/templates/base.vm index d21659c..82647f8 100644 --- a/src/main/resources/views/templates/base.vm +++ b/src/main/resources/views/templates/base.vm @@ -87,7 +87,7 @@ #end
  • Custom Commands + Custom Commands
  • #else ## TODO: use a different tactic for this