Skip to content

Commit

Permalink
Implement fetching guilds for current OAuth user
Browse files Browse the repository at this point in the history
  • Loading branch information
schnapster committed May 12, 2024
1 parent 9b417f5 commit 3a9ff0a
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 11 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ plugins {
}

group = "dev.capybaralabs.shipa"
version = "0.6.2"
version = "0.6.3"

allprojects {
java {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.capybaralabs.shipa.discord.client

import dev.capybaralabs.shipa.discord.oauth2.OAuth2Scope

sealed interface DiscordAuthToken {

val token: String
Expand All @@ -16,7 +18,9 @@ sealed interface DiscordAuthToken {

data class Oauth2(
override val token: String,
val scopes: List<OAuth2Scope>,
) : DiscordAuthToken {

override fun authHeader(): String {
return "Bearer $token"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import dev.capybaralabs.shipa.ShipaMetrics
import dev.capybaralabs.shipa.discord.client.ratelimit.Bucket
import dev.capybaralabs.shipa.discord.client.ratelimit.BucketKey
import dev.capybaralabs.shipa.discord.client.ratelimit.BucketService
import dev.capybaralabs.shipa.discord.oauth2.OAuth2Scope
import dev.capybaralabs.shipa.discord.oauth2.OAuth2ScopeException
import dev.capybaralabs.shipa.logger
import io.prometheus.client.Collector
import java.time.Duration
Expand Down Expand Up @@ -62,6 +64,20 @@ class DiscordRestService(
return DiscordRestService(authToken, restTemplateBuilder, bucketService, metrics)
}

/**
* @throws OAuth2ScopeException if the token is a user token and is missing the required scope
*/
fun assertUserHasScope(scope: OAuth2Scope) {
when (authToken) {
is DiscordAuthToken.Bot -> {} // is this a sane assumption? how do we know the bot can access the concrete endpoint?
is DiscordAuthToken.Oauth2 -> {
if (!authToken.scopes.contains(scope)) {
throw OAuth2ScopeException(authToken, scope)
}
}
}
}

/**
* [Discord Rate Limits](https://discord.com/developers/docs/topics/rate-limits#rate-limits)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package dev.capybaralabs.shipa.discord.client.entity
import dev.capybaralabs.shipa.discord.DiscordProperties
import dev.capybaralabs.shipa.discord.client.DiscordRestService
import dev.capybaralabs.shipa.discord.client.ratelimit.UsersId
import dev.capybaralabs.shipa.discord.client.ratelimit.UsersIdGuilds
import dev.capybaralabs.shipa.discord.client.ratelimit.UsersMe
import dev.capybaralabs.shipa.discord.model.Channel
import dev.capybaralabs.shipa.discord.model.PartialGuild
import dev.capybaralabs.shipa.discord.model.User
import dev.capybaralabs.shipa.discord.oauth2.OAuth2Scope.GUILDS
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import org.springframework.http.RequestEntity
Expand Down Expand Up @@ -64,4 +67,16 @@ class DiscordUserRestService(
).body!!
}

// https://discord.com/developers/docs/resources/user#get-current-user-guilds
suspend fun listCurrentUserGuilds(): List<PartialGuild> {
discordRestService.assertUserHasScope(GUILDS)

return discordRestService.exchange<List<PartialGuild>>(
UsersIdGuilds,
RequestEntity
.get("/users/@me/guilds")
.build(),
).body!!
}

}
13 changes: 3 additions & 10 deletions src/main/kotlin/dev/capybaralabs/shipa/discord/model/Guild.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import kotlin.jvm.optionals.getOrNull
* [Discord Guild Object](https://discord.com/developers/docs/resources/guild#guild-object)
*/
data class Guild(
val id: Long,
override val id: Long,
val name: String,
val icon: Optional<String>,
override val icon: Optional<String>,
val iconHash: Optional<String>?,
val splash: Optional<String>,
val discoverySplash: Optional<String>,
Expand Down Expand Up @@ -50,14 +50,7 @@ data class Guild(
val nsfwLevel: GuildNsfwLevel,
// val stickers: List<Sticker>,
val premiumProgressBarEnabled: Boolean,
) {

fun iconUrl(): String? {
return icon.getOrNull()?.let {
val format = if (it.startsWith("a_")) GIF else PNG
ImageFormatting.imageUrl("/icons/$id/$it", format)
}
}
) : HasGuildIcon {

fun splashUrl(): String? {
return splash.getOrNull()?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.capybaralabs.shipa.discord.model

import dev.capybaralabs.shipa.discord.model.ImageFormatting.Format.GIF
import dev.capybaralabs.shipa.discord.model.ImageFormatting.Format.PNG
import java.util.Optional
import kotlin.jvm.optionals.getOrNull

interface HasGuildIcon {
val id: Long
val icon: Optional<String>

fun iconUrl(): String? {
return icon.getOrNull()?.let {
val format = if (it.startsWith("a_")) GIF else PNG
ImageFormatting.imageUrl("/icons/$id/$it", format)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.capybaralabs.shipa.discord.model

import java.util.Optional

/**
* [Partial Discord Guild Object](https://discord.com/developers/docs/resources/user#get-current-user-guilds-example-partial-guild)
*/
data class PartialGuild(
override val id: Long,
val name: String,
override val icon: Optional<String>,
val owner: Boolean?,
val permissions: StringBitfield<Permission>?,
val features: List<String>, // don't use a enum, these change frequently and many are undocumented
val approximateMemberCount: Int?,
val approximatePresenceCount: Int?,
) : HasGuildIcon
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.capybaralabs.shipa.discord.oauth2


/**
* See [Discord OAuth2 Scopes](https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes) for the full list.
*/
enum class OAuth2Scope(val discordName: String) {

IDENTIFY("identify"),
GUILDS("guilds"),
;

companion object {
fun parse(input: String): OAuth2Scope? {
for (scope in entries) {
if (scope.discordName.equals(input, ignoreCase = true)) {
return scope
}
if (scope.name.equals(input, ignoreCase = true)) {
return scope
}
}
return null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.capybaralabs.shipa.discord.oauth2

import dev.capybaralabs.shipa.discord.client.DiscordAuthToken

/**
* The used token is lacking a scope to access the requested Discord resource.
*/
class OAuth2ScopeException(
val token: DiscordAuthToken.Oauth2,
val missingScope: OAuth2Scope,
) : RuntimeException(
"Token is missing scope $missingScope, has scopes ${token.scopes}",
)

0 comments on commit 3a9ff0a

Please sign in to comment.