From 27bdb6debc6004e422886ce3f082d011c03e81bd Mon Sep 17 00:00:00 2001 From: WillDotWhite Date: Fri, 6 Sep 2024 10:48:43 +0100 Subject: [PATCH] Womp womp --- .../com/gmtkgamejam/bot/BotMessageBuilder.kt | 52 --- .../com/gmtkgamejam/discord/HttpClient.kt | 19 - .../com/gmtkgamejam/discord/RefreshToken.kt | 28 -- .../com/gmtkgamejam/koin/DatabaseModule.kt | 17 - .../com/gmtkgamejam/koin/DiscordBotModule.kt | 10 - .../gmtkgamejam/models/admin/BannedUser.kt | 11 - .../models/admin/dtos/BanUnbanUserDto.kt | 9 - .../models/admin/dtos/DeletePostDto.kt | 8 - .../admin/dtos/ReportedUsersClearDto.kt | 8 - .../models/analytivs/AnalyticsCommonEvents.kt | 7 - .../models/analytivs/AnalyticsViewEvent.kt | 6 - .../gmtkgamejam/models/bot/dtos/BotDmDto.kt | 8 - .../gmtkgamejam/models/posts/Availability.kt | 29 -- .../models/posts/FavouritesList.kt | 9 - .../com/gmtkgamejam/models/posts/PostItem.kt | 69 ---- .../com/gmtkgamejam/models/posts/Skills.kt | 17 - .../com/gmtkgamejam/models/posts/Tools.kt | 19 - .../models/posts/dtos/FavouritePostDto.kt | 13 - .../models/posts/dtos/PostItemCreateDto.kt | 23 -- .../models/posts/dtos/PostItemDeleteDto.kt | 11 - .../models/posts/dtos/PostItemReportDto.kt | 11 - .../dtos/PostItemUnableToContactReportDto.kt | 12 - .../models/posts/dtos/PostItemUpdateDto.kt | 22 -- .../gmtkgamejam/models/posts/dtos/PostsDTO.kt | 10 - .../repositories/AdminRepository.kt | 33 -- .../repositories/AnalyticsRepository.kt | 42 --- .../repositories/FavouritesRepository.kt | 31 -- .../repositories/PostRepository.kt | 92 ----- .../com/gmtkgamejam/routing/AdminRoutes.kt | 76 ---- .../gmtkgamejam/routing/DiscordBotRoutes.kt | 116 ------ .../gmtkgamejam/routing/FavouritesRoutes.kt | 47 --- .../com/gmtkgamejam/routing/PostRoutes.kt | 331 ------------------ .../com/gmtkgamejam/services/AdminService.kt | 19 - .../gmtkgamejam/services/AnalyticsService.kt | 28 -- .../gmtkgamejam/services/FavouritesService.kt | 28 -- .../com/gmtkgamejam/services/PostService.kt | 50 --- .../Application.kt | 36 +- .../ApplicationResponseExtensions.kt | 2 +- .../com/gmtkvotingsystem/AuthFromRequest.kt | 21 ++ .../AuthModule.kt | 8 +- .../CollectionExtensions.kt | 4 +- .../Config.kt | 2 +- .../EnumExtensions.kt | 6 +- .../bot/DiscordBot.kt | 76 +--- .../gmtkvotingsystem/discord/HttpClient.kt | 20 ++ .../gmtkvotingsystem/discord/RefreshToken.kt | 30 ++ .../discord/UserInfo.kt | 12 +- .../gmtkvotingsystem/koin/DatabaseModule.kt | 13 + .../gmtkvotingsystem/koin/DiscordBotModule.kt | 11 + .../com/gmtkvotingsystem/models/Theme.kt | 13 + .../com/gmtkvotingsystem/models/ThemeDTO.kt | 8 + .../models/ThemeVotedOnDTO.kt | 11 + .../com/gmtkvotingsystem/models/Vote.kt | 12 + .../com/gmtkvotingsystem/models/VoteDTO.kt | 9 + .../com/gmtkvotingsystem/models/VotesDTO.kt | 8 + .../models/auth/AuthTokenSet.kt | 2 +- .../auth/DiscordRefreshTokenResponse.kt | 4 +- .../models/auth/DiscordUserInfo.kt | 2 +- .../models/auth/UserInfo.kt | 11 +- .../routing/AuthRoutes.kt | 44 +-- .../gmtkvotingsystem/routing/ThemeRoutes.kt | 37 ++ .../routing/UserInfoRoutes.kt | 40 +-- .../gmtkvotingsystem/routing/VoteRoutes.kt | 40 +++ .../services/AuthService.kt | 15 +- .../gmtkvotingsystem/services/ThemeService.kt | 61 ++++ 65 files changed, 399 insertions(+), 1480 deletions(-) delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/bot/BotMessageBuilder.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/discord/HttpClient.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/discord/RefreshToken.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/koin/DatabaseModule.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/koin/DiscordBotModule.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/admin/BannedUser.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/BanUnbanUserDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/DeletePostDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/ReportedUsersClearDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsCommonEvents.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsViewEvent.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/bot/dtos/BotDmDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/Availability.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/FavouritesList.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/PostItem.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/Skills.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/Tools.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/FavouritePostDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemCreateDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemDeleteDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemReportDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUnableToContactReportDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUpdateDto.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostsDTO.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/repositories/AdminRepository.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/repositories/AnalyticsRepository.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/repositories/FavouritesRepository.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/repositories/PostRepository.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/routing/AdminRoutes.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/routing/DiscordBotRoutes.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/routing/FavouritesRoutes.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/services/AdminService.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/services/AnalyticsService.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/services/FavouritesService.kt delete mode 100644 api/src/main/kotlin/com/gmtkgamejam/services/PostService.kt rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/Application.kt (66%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/ApplicationResponseExtensions.kt (91%) create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/AuthFromRequest.kt rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/AuthModule.kt (95%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/CollectionExtensions.kt (98%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/Config.kt (95%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/EnumExtensions.kt (66%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/bot/DiscordBot.kt (50%) create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/discord/HttpClient.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/discord/RefreshToken.kt rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/discord/UserInfo.kt (72%) create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/koin/DatabaseModule.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/koin/DiscordBotModule.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/models/Theme.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeDTO.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeVotedOnDTO.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/models/Vote.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/models/VoteDTO.kt create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/models/VotesDTO.kt rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/models/auth/AuthTokenSet.kt (92%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/models/auth/DiscordRefreshTokenResponse.kt (84%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/models/auth/DiscordUserInfo.kt (84%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/models/auth/UserInfo.kt (71%) rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/routing/AuthRoutes.kt (60%) create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/routing/ThemeRoutes.kt rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/routing/UserInfoRoutes.kt (67%) create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/routing/VoteRoutes.kt rename api/src/main/kotlin/com/{gmtkgamejam => gmtkvotingsystem}/services/AuthService.kt (79%) create mode 100644 api/src/main/kotlin/com/gmtkvotingsystem/services/ThemeService.kt diff --git a/api/src/main/kotlin/com/gmtkgamejam/bot/BotMessageBuilder.kt b/api/src/main/kotlin/com/gmtkgamejam/bot/BotMessageBuilder.kt deleted file mode 100644 index fe504bcb..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/bot/BotMessageBuilder.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.gmtkgamejam.bot - -import com.gmtkgamejam.services.PostService -import org.javacord.api.entity.message.embed.EmbedBuilder -import org.javacord.api.entity.user.User - -class BotMessageBuilder { - - private val postService = PostService() - - fun canBuildEmbedFromUser(sender: User): Boolean = postService.getPostByAuthorId(sender.id.toString()) != null - - fun embedMessage(recipient: User, sender: User): EmbedBuilder { - val post = postService.getPostByAuthorId(sender.id.toString())!! - - val shortDescription = if (post.description.length > 240) post.description.take(237) + "..." else post.description - - val embed = EmbedBuilder() - .setTitle("${sender.name} wants to get in contact!") - .setDescription("Hey there ${recipient.name}! ${sender.name} wants to get in touch - this is a summary of their current post on the Team Finder!") - .setAuthor("GMTK Team Finder", "https://findyourjam.team/", "https://findyourjam.team/logos/jam-logo-stacked.webp") - .addField("Description", shortDescription) - - // Add optional fields - turns out this includes timezones - if (post.skillsSought?.isNotEmpty() == true) { - embed.addField("${sender.name} is looking for:", post.skillsSought.toString()) - } - if (post.skillsPossessed?.isNotEmpty() == true) { - embed.addField("${sender.name} can bring:", post.skillsPossessed.toString()) - } - if (post.preferredTools?.isNotEmpty() == true) { - embed.addField("Engine(s)", post.preferredTools.toString()) - } - if (post.timezoneOffsets.isNotEmpty()) { - embed.addField("Timezone(s)", post.timezoneOffsets.map { if (it < 0) "UTC-$it" else "UTC+$it" }.toString()) - } - - embed - .addField("Like what you see?", "Check out their full post here to see more! https://findyourjam.team/gmtk/${post.id}/") - .setFooter("Feedback? DM @dotwo in the #developing-gtmk-team-finder-app channel") - - return embed - } - - // TODO: Add a variety of messages to mix things up a bit? - fun basicMessage(recipient: User, sender: User): String { - return """ - Hey ${recipient.mentionTag}, ${sender.mentionTag} wants to get in contact about your Team Finder post! - They don't have a post on the Team Finder yet, so why not drop them a message and find out more? - """.trimIndent() - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/discord/HttpClient.kt b/api/src/main/kotlin/com/gmtkgamejam/discord/HttpClient.kt deleted file mode 100644 index 02f20919..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/discord/HttpClient.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.gmtkgamejam.discord - -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import kotlinx.serialization.json.Json -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.kotlinx.json.* - -fun discordHttpClient(): HttpClient { - return HttpClient(CIO) { - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - isLenient = true - prettyPrint = true - }) - } - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/discord/RefreshToken.kt b/api/src/main/kotlin/com/gmtkgamejam/discord/RefreshToken.kt deleted file mode 100644 index c2793a7f..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/discord/RefreshToken.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.gmtkgamejam.discord - -import com.gmtkgamejam.models.auth.DiscordRefreshTokenResponse -import io.ktor.client.call.body -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.http.* - -const val refreshTokenEndpoint = "https://discord.com/api/oauth2/token" - -suspend fun refreshTokenAsync( - clientId: String, - clientSecret: String, - refreshToken: String -): DiscordRefreshTokenResponse { - return discordHttpClient().use { client -> - client - .post(refreshTokenEndpoint) { - setBody(FormDataContent(Parameters.build { - append("client_id", clientId) - append("client_secret", clientSecret) - append("grant_type", "refresh_token") - append("refresh_token", refreshToken) - })) - } - .body() - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/koin/DatabaseModule.kt b/api/src/main/kotlin/com/gmtkgamejam/koin/DatabaseModule.kt deleted file mode 100644 index e251f8fe..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/koin/DatabaseModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.gmtkgamejam.koin - -import com.gmtkgamejam.Config -import com.gmtkgamejam.repositories.* -import org.koin.dsl.module -import org.litote.kmongo.KMongo - -val DatabaseModule = module(createdAtStart = true) { - single { - val url = Config.getString("secrets.database.url") - KMongo.createClient(url) - } - single { AdminRepositoryImpl(get()) } - single { AnalyticsRepositoryImpl(get()) } - single { FavouritesRepositoryImpl(get()) } - single { PostRepositoryImpl(get()) } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/koin/DiscordBotModule.kt b/api/src/main/kotlin/com/gmtkgamejam/koin/DiscordBotModule.kt deleted file mode 100644 index bd098555..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/koin/DiscordBotModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.gmtkgamejam.koin - -import com.gmtkgamejam.bot.DiscordBot -import org.koin.dsl.module - -val DiscordBotModule = module(createdAtStart = true) { - single { - DiscordBot() - } -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/admin/BannedUser.kt b/api/src/main/kotlin/com/gmtkgamejam/models/admin/BannedUser.kt deleted file mode 100644 index 05d99c6a..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/admin/BannedUser.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.gmtkgamejam.models.admin - -import com.gmtkgamejam.models.admin.dtos.BanUnbanUserDto - -data class BannedUser( - val discordId: String, - val banningAdminId: String, - var bannedAt: String?, -) { - constructor(dto: BanUnbanUserDto) : this(dto.discordId, dto.adminId, null) -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/BanUnbanUserDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/BanUnbanUserDto.kt deleted file mode 100644 index 02064d72..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/BanUnbanUserDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.gmtkgamejam.models.admin.dtos - -import kotlinx.serialization.Serializable - -@Serializable -data class BanUnbanUserDto( - val discordId: String, - val adminId: String, -) \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/DeletePostDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/DeletePostDto.kt deleted file mode 100644 index f8b19085..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/DeletePostDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.gmtkgamejam.models.admin.dtos - -import kotlinx.serialization.Serializable - -@Serializable -data class DeletePostDto( - var postId: String, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/ReportedUsersClearDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/ReportedUsersClearDto.kt deleted file mode 100644 index d35dba34..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/admin/dtos/ReportedUsersClearDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.gmtkgamejam.models.admin.dtos - -import kotlinx.serialization.Serializable - -@Serializable -data class ReportedUsersClearDto( - var teamId: String, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsCommonEvents.kt b/api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsCommonEvents.kt deleted file mode 100644 index 8901d849..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsCommonEvents.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.gmtkgamejam.models.analytivs - -// There will only be one of these in the DB -data class AnalyticsCommonEvents( - val id: String = "events", - var loginCount: Int, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsViewEvent.kt b/api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsViewEvent.kt deleted file mode 100644 index 29290579..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/analytivs/AnalyticsViewEvent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.gmtkgamejam.models.analytivs - -data class AnalyticsViewEvent( - val query: String, - var count: Int = 0 -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/bot/dtos/BotDmDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/bot/dtos/BotDmDto.kt deleted file mode 100644 index 3b6cafe8..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/bot/dtos/BotDmDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.gmtkgamejam.models.bot.dtos - -import kotlinx.serialization.Serializable - -@Serializable -data class BotDmDto( - val recipientId: String -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/Availability.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/Availability.kt deleted file mode 100644 index 381731ee..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/Availability.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.gmtkgamejam.models.posts - -enum class Availability { - - /** - * Haven't decided/don't know yet - */ - UNSURE, - - /** - * A few hours over the whole jam - */ - MINIMAL, - - /** - * Less than 4 hours per day - */ - PART_TIME, - - /** - * 4-8 hours per day - */ - FULL_TIME, - - /** - * As much time as I can - */ - OVERTIME, -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/FavouritesList.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/FavouritesList.kt deleted file mode 100644 index 14f2c0c0..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/FavouritesList.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.gmtkgamejam.models.posts - -import kotlinx.serialization.Serializable - -@Serializable -data class FavouritesList( - val discordId: String, - val postIds: MutableList = mutableListOf(), -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/PostItem.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/PostItem.kt deleted file mode 100644 index 7f46e03f..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/PostItem.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.gmtkgamejam.models.posts - -import com.gmtkgamejam.models.posts.dtos.PostItemCreateDto -import kotlinx.serialization.Serializable -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.concurrent.ThreadLocalRandom -import kotlin.math.abs - -@Serializable -data class PostItem ( - val id: String, - - var author: String, - var authorId: String, - - var description: String, - var size: Int, - - var skillsPossessed: Set?, - var skillsSought: Set?, - - var preferredTools: Set?, - var availability: Availability, - var timezoneOffsets: Set, - var languages: Set, - - var queryCount: Int, - var fullPageViewCount: Int, - // Reported by the flag icon for inappropriate content - var reportCount: Int, - - // Optional flag for users to report the linked profile as - // unabled to be contacted by discord CTA - var unableToContactCount: Int, - - // Managed by DB - val createdAt: String, - var updatedAt: String, - var deletedAt: String?, -) { - var isFavourite: Boolean = false - companion object { - fun fromCreateDto(dto: PostItemCreateDto): PostItem { - // TODO: Standardise datetime format - val currentDatetime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - return PostItem( - abs(ThreadLocalRandom.current().nextLong()).toString(), // We need to handle as string, otherwise we lose precision in JS - dto.author, - dto.authorId, - dto.description.take(2000), - dto.size, - dto.skillsPossessed, - dto.skillsSought, - dto.preferredTools, - dto.availability, - dto.timezoneOffsets.filter { it >= -12 && it <= 12 }.toSet(), - dto.languages, - 0, - 0, - 0, - 0, - currentDatetime, - currentDatetime, - null - ) - } - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/Skills.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/Skills.kt deleted file mode 100644 index df82a772..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/Skills.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.gmtkgamejam.models.posts - -enum class Skills(private var readableName: String) { - ART_2D("2D Art"), - ART_3D("3D Art"), - CODE("Code"), - DESIGN_PRODUCTION("Design/Production"), - SFX("SFX"), - MUSIC("Music"), - TESTING_SUPPORT("Testing/Support"), - TEAM_LEAD("Team lead"), - OTHER("Other"); - - override fun toString(): String { - return readableName; - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/Tools.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/Tools.kt deleted file mode 100644 index 0fa81a27..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/Tools.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.gmtkgamejam.models.posts - -// For the time being, Tool == Engine -enum class Tools(private var readableName: String) { - UNITY("Unity"), - CONSTRUCT("Construct"), - GAME_MAKER_STUDIO("Game Maker Studio"), - GODOT("Godot"), - TWINE("Twine"), - BITSY("Bitsy"), - UNREAL("Unreal"), - RPG_MAKER("RPG Maker"), - PICO_8("PICO 8"), - OTHER("Other"); - - override fun toString(): String { - return readableName; - } -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/FavouritePostDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/FavouritePostDto.kt deleted file mode 100644 index 10b8b02f..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/FavouritePostDto.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import kotlinx.serialization.Serializable - -/** - * This model is the payload for adding/removing a new Favourite item - * - * This is only triggered for one post at a time - */ -@Serializable -data class FavouritePostDto ( - var postId: String, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemCreateDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemCreateDto.kt deleted file mode 100644 index d9287632..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemCreateDto.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import com.gmtkgamejam.models.posts.Availability -import com.gmtkgamejam.models.posts.Skills -import com.gmtkgamejam.models.posts.Tools -import kotlinx.serialization.Serializable - -/** - * This model is the payload for a new PostItem being created on POST /posts - */ -@Serializable -data class PostItemCreateDto( - var author: String, - var authorId: String, - var description: String, - var size: Int, - var skillsPossessed: Set, - var skillsSought: Set, - var preferredTools: Set?, - var availability: Availability, - var timezoneOffsets: Set, - var languages: Set, -) \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemDeleteDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemDeleteDto.kt deleted file mode 100644 index 49f93c80..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemDeleteDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import kotlinx.serialization.Serializable - -/** - * This model is the payload for deleting a PostItem - */ -@Serializable -data class PostItemDeleteDto ( - var id: Long, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemReportDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemReportDto.kt deleted file mode 100644 index 30f4e673..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemReportDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import kotlinx.serialization.Serializable - -/** - * This model is the payload for reporting a PostItem - */ -@Serializable -data class PostItemReportDto ( - var id: String, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUnableToContactReportDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUnableToContactReportDto.kt deleted file mode 100644 index 8819e493..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUnableToContactReportDto.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import kotlinx.serialization.Serializable - -/** - * This model is the payload for reporting a PostItem - * as unabled to be contacted by discord CTA - */ -@Serializable -data class PostItemUnableToContactReportDto ( - var id: String, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUpdateDto.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUpdateDto.kt deleted file mode 100644 index 0136919e..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostItemUpdateDto.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import com.gmtkgamejam.models.posts.Availability -import com.gmtkgamejam.models.posts.Skills -import com.gmtkgamejam.models.posts.Tools -import kotlinx.serialization.Serializable - -/** - * This model is the payload for an existing PostItem being updated on PUT /posts/mine - */ -@Serializable -data class PostItemUpdateDto ( - var author: String?, - var description: String?, - var size: Int?, - var skillsPossessed: Set?, - var skillsSought: Set?, - var preferredTools: Set?, - var availability: Availability?, - var timezoneOffsets: Set?, - var languages: Set?, -) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostsDTO.kt b/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostsDTO.kt deleted file mode 100644 index a067aca8..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/models/posts/dtos/PostsDTO.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.gmtkgamejam.models.posts.dtos - -import com.gmtkgamejam.models.posts.PostItem -import kotlinx.serialization.Serializable - -@Serializable -data class PostsDTO( - val posts: List, - val pagination: Map -) \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/repositories/AdminRepository.kt b/api/src/main/kotlin/com/gmtkgamejam/repositories/AdminRepository.kt deleted file mode 100644 index dfd23379..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/repositories/AdminRepository.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.gmtkgamejam.repositories - -import com.gmtkgamejam.models.admin.BannedUser -import com.mongodb.client.MongoClient -import com.mongodb.client.MongoCollection -import org.litote.kmongo.eq -import org.litote.kmongo.getCollectionOfName -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -interface AdminRepository { - fun banUser(bannedUser: BannedUser) - fun unbanUser(bannedUser: BannedUser) -} - -class AdminRepositoryImpl(val client: MongoClient) : AdminRepository { - private val bannedUserCol: MongoCollection - - init { - val database = client.getDatabase("team-finder") - bannedUserCol = database.getCollectionOfName("banned-users") - } - - override fun banUser(bannedUser: BannedUser) { - bannedUser.bannedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - bannedUserCol.insertOne(bannedUser) - } - - override fun unbanUser(bannedUser: BannedUser) { - bannedUserCol.deleteMany(BannedUser::discordId eq bannedUser.discordId) - } - -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/repositories/AnalyticsRepository.kt b/api/src/main/kotlin/com/gmtkgamejam/repositories/AnalyticsRepository.kt deleted file mode 100644 index 6cce7d3c..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/repositories/AnalyticsRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.gmtkgamejam.repositories - -import com.gmtkgamejam.models.analytivs.AnalyticsCommonEvents -import com.gmtkgamejam.models.analytivs.AnalyticsViewEvent -import com.mongodb.client.MongoClient -import com.mongodb.client.MongoCollection -import com.mongodb.client.model.UpdateOptions -import org.litote.kmongo.eq -import org.litote.kmongo.findOne -import org.litote.kmongo.getCollectionOfName -import org.litote.kmongo.updateOne - -interface AnalyticsRepository { - fun trackQuery(queryParams: Map) - fun trackLogin() -} - -open class AnalyticsRepositoryImpl(val client: MongoClient) : AnalyticsRepository { - protected val col: MongoCollection = client - .getDatabase("team-finder") - .getCollectionOfName("analytics-events") - - protected val viewCol: MongoCollection = client - .getDatabase("team-finder") - .getCollectionOfName("analytics-view-events") - - override fun trackQuery(queryParams: Map) { - val key = queryParams.toString() - - val record = viewCol.findOne(AnalyticsViewEvent::query eq key) ?: AnalyticsViewEvent(key, 0) - record.count += 1 - - viewCol.updateOne(AnalyticsViewEvent::query eq key, record, UpdateOptions().upsert(true)) - } - - override fun trackLogin() { - val record: AnalyticsCommonEvents = col.findOne(AnalyticsCommonEvents::id eq "events") ?: AnalyticsCommonEvents("events", 0) - record.loginCount += 1 - - col.updateOne(AnalyticsCommonEvents::id eq "events", record, UpdateOptions().upsert(true)) - } -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/repositories/FavouritesRepository.kt b/api/src/main/kotlin/com/gmtkgamejam/repositories/FavouritesRepository.kt deleted file mode 100644 index e21db93f..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/repositories/FavouritesRepository.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.gmtkgamejam.repositories - -import com.gmtkgamejam.models.auth.AuthTokenSet -import com.gmtkgamejam.models.posts.FavouritesList -import com.mongodb.client.MongoClient -import com.mongodb.client.MongoCollection -import com.mongodb.client.model.UpdateOptions -import org.litote.kmongo.eq -import org.litote.kmongo.findOne -import org.litote.kmongo.getCollectionOfName -import org.litote.kmongo.updateOne - - -interface FavouritesRepository { - fun getFavouritesByUserId(discordId: String): FavouritesList? - fun saveFavourites(favouritesToSave: FavouritesList) -} - -open class FavouritesRepositoryImpl(val client: MongoClient) : FavouritesRepository { - protected val col: MongoCollection = client - .getDatabase("team-finder") - .getCollectionOfName("favourites") - - override fun getFavouritesByUserId(discordId: String): FavouritesList? { - return col.findOne(FavouritesList::discordId eq discordId) - } - - override fun saveFavourites(favouritesToSave: FavouritesList) { - col.updateOne(AuthTokenSet::discordId eq favouritesToSave.discordId, favouritesToSave, UpdateOptions().upsert(true)) - } -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/repositories/PostRepository.kt b/api/src/main/kotlin/com/gmtkgamejam/repositories/PostRepository.kt deleted file mode 100644 index b20d3643..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/repositories/PostRepository.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.gmtkgamejam.repositories - -import com.gmtkgamejam.models.admin.BannedUser -import com.gmtkgamejam.models.posts.PostItem -import com.mongodb.client.MongoClient -import com.mongodb.client.MongoCollection -import org.bson.conversions.Bson -import org.litote.kmongo.* -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -interface PostRepository { - fun createPost(postItem: PostItem) - fun getPosts(filter: Bson, sort: Bson, page: Int): List - fun getPost(id: String) : PostItem? - fun getPostByAuthorId(authorId: String, ignoreDeletion: Boolean = false) : PostItem? - fun updatePost(postItem: PostItem) - fun deletePost(postItem: PostItem) - fun addQueryView(postItem: PostItem) - fun addFullPageView(postItem: PostItem) - fun getPostCount(filter: Bson): Int - - companion object { - const val PAGE_SIZE = 36 - } -} - -open class PostRepositoryImpl(val client: MongoClient) : PostRepository { - private val col: MongoCollection = client - .getDatabase("team-finder") - .getCollectionOfName("posts") - - private val bannedUsersCol: MongoCollection = client - .getDatabase("team-finder") - .getCollectionOfName("banned-users") - - - override fun createPost(postItem: PostItem) { - if (bannedUsersCol.findOne(BannedUser::discordId eq postItem.authorId) != null) { - throw Exception("User is banned, cannot perform action!") - } - - col.insertOne(postItem) - } - - // Un-paginated version should be used for Admin endpoints - override fun getPosts(filter: Bson, sort: Bson, page: Int): List { - val pageSize = PostRepository.PAGE_SIZE - return col.find(filter).sort(sort).limit(pageSize).skip(pageSize * (page - 1)).toList() - } - - override fun getPost(id: String) : PostItem? { - return col.findOne(PostItem::id eq id) - } - - override fun getPostByAuthorId(authorId: String, ignoreDeletion: Boolean) : PostItem? { - var filter = PostItem::authorId eq authorId - if (!ignoreDeletion) { - filter = and(filter, PostItem::deletedAt eq null) - } - - return col.findOne(filter) - } - - override fun getPostCount(filter: Bson): Int { - return col.countDocuments(and(filter, PostItem::deletedAt eq null)).toInt(); - } - - override fun updatePost(postItem: PostItem) { - if (bannedUsersCol.findOne(BannedUser::discordId eq postItem.authorId) != null) { - throw Exception("User is banned, cannot perform action!") - } - - col.updateOne(PostItem::id eq postItem.id, postItem) - } - - override fun deletePost(postItem: PostItem) { - postItem.deletedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - col.updateOne(PostItem::id eq postItem.id, postItem) - } - - override fun addQueryView(postItem: PostItem) { - postItem.queryCount += 1 - col.updateOne(PostItem::id eq postItem.id, postItem) - } - - override fun addFullPageView(postItem: PostItem) { - postItem.fullPageViewCount += 1 - col.updateOne(PostItem::id eq postItem.id, postItem) - } - -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/AdminRoutes.kt b/api/src/main/kotlin/com/gmtkgamejam/routing/AdminRoutes.kt deleted file mode 100644 index 00d76be5..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/AdminRoutes.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.gmtkgamejam.routing - -import com.gmtkgamejam.models.admin.BannedUser -import com.gmtkgamejam.models.admin.dtos.BanUnbanUserDto -import com.gmtkgamejam.models.admin.dtos.DeletePostDto -import com.gmtkgamejam.models.admin.dtos.ReportedUsersClearDto -import com.gmtkgamejam.models.posts.PostItem -import com.gmtkgamejam.respondJSON -import com.gmtkgamejam.services.AdminService -import com.gmtkgamejam.services.PostService -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.litote.kmongo.and -import org.litote.kmongo.descending -import org.litote.kmongo.eq -import org.litote.kmongo.gt - -fun Application.configureAdminRouting() { - - val service = PostService() - val adminService = AdminService() - - routing { - authenticate("auth-jwt-admin") { - route("/admin") { - route("/reports") { - get { - val filters = mutableListOf(PostItem::deletedAt eq null, PostItem::reportCount gt 0) - call.respond(service.getPosts(and(filters), descending(PostItem::reportCount), 1)) - } - post("/clear") { - val data = call.receive() - service.getPost(data.teamId)?.let { - it.reportCount = 0 - service.updatePost(it) - return@post call.respond(it) - } - - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - } - route("/post") { - delete { - val data = call.receive() - service.getPost(data.postId)?.let { - service.deletePost(it) - return@delete call.respond(it) - } - - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - } - route("/user") { - post("/ban") { - val data = call.receive() - val userToBan = BannedUser(data) - adminService.banUser(userToBan).let { - return@post call.respond("User banned") - } - } - post("/unban") { - val data = call.receive() - val userToBan = BannedUser(data) - adminService.unbanUser(userToBan).let { - return@post call.respond("User unbanned") - } - } - } - } - } - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/DiscordBotRoutes.kt b/api/src/main/kotlin/com/gmtkgamejam/routing/DiscordBotRoutes.kt deleted file mode 100644 index 180a084a..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/DiscordBotRoutes.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.gmtkgamejam.routing - -import com.gmtkgamejam.Config -import com.gmtkgamejam.bot.DiscordBot -import com.gmtkgamejam.models.bot.dtos.BotDmDto -import com.gmtkgamejam.respondJSON -import com.gmtkgamejam.services.AuthService -import com.gmtkgamejam.toJsonElement -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.koin.ktor.ext.inject -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.time.LocalDateTime - - -fun Application.configureDiscordBotRouting() { - - val logger: Logger = LoggerFactory.getLogger(javaClass) - - val authService = AuthService() - val bot: DiscordBot by inject() - - val userIdMessageTimes: MutableMap = mutableMapOf() - - // Sender/Recipient Pair is stored as a String to allow for easier handling of JSON in monitoring - val userIdPerUserMessageTimes: MutableMap = mutableMapOf() - - // User A needs to wait X seconds between messaging User B and User C - val userRateLimitTimeOutInSeconds = Config.getString("bot.userRateLimit").toLong() - - // User A can't ping User B more than once per X seconds - val perUserTimeoutInSeconds = Config.getString("bot.perRecipientRateLimit").toLong() - - fun canUserSendMessage(userId: String): Boolean { - val currentDateTime = LocalDateTime.now() - val previousMessageDateTime = userIdMessageTimes[userId] ?: LocalDateTime.MIN - - return currentDateTime.isAfter(previousMessageDateTime.plusSeconds(userRateLimitTimeOutInSeconds)) - } - - fun canUserSendMessageToThisUser(senderId: String, recipientId: String): Boolean { - val currentDateTime = LocalDateTime.now() - - val specificRecipientKey = Pair(senderId, recipientId).toString() - val previousMessageDateTime = userIdPerUserMessageTimes[specificRecipientKey] ?: LocalDateTime.MIN - - return currentDateTime.isAfter(previousMessageDateTime.plusSeconds(perUserTimeoutInSeconds)) - } - - routing { - authenticate("auth-jwt") { - route("/bot") { - post("/dm") { - val data = call.receive() - - val tokenSet = authService.getTokenSet(call) ?: return@post call.respondJSON( - "Your request couldn't be authorised", - status = HttpStatusCode.Unauthorized - ) - - val senderId = tokenSet.discordId - val recipientId = data.recipientId - - if (!canUserSendMessageToThisUser(senderId, recipientId)) { - return@post call.respondJSON( - "You can't message a single user again so quickly", - status = HttpStatusCode.TooManyRequests - ) - } - - if (!canUserSendMessage(senderId)) { - return@post call.respondJSON( - "You are sending too many messages - please wait a few minutes and try again", - status = HttpStatusCode.TooManyRequests - ) - } - - try { - val sendTime = LocalDateTime.now() - - bot.createContactUserPingMessage(recipientId, senderId) - userIdMessageTimes[senderId] = sendTime - userIdPerUserMessageTimes[Pair(senderId, recipientId).toString()] = sendTime - logger.error("Sender [$senderId] has pinged Recipient [$recipientId] at [$sendTime]") - return@post call.respond(it) - } catch (ex: Exception) { - logger.error("Could not create ping message: $ex") - return@post call.respondJSON( - "This message could not be sent, please inform the Team Finder Support group in Discord", - status = HttpStatusCode.NotAcceptable - ) - } - } - - authenticate("auth-jwt-admin") { - get("/_monitoring") { - val data = mapOf( - "userTimeout" to userRateLimitTimeOutInSeconds, - "perRecipientTimeout" to perUserTimeoutInSeconds, - "userIdMessageTimes" to userIdMessageTimes, - "userIdPerUserMessageTimes" to userIdPerUserMessageTimes, - ) - - // .toJsonElement required as Ktor can't serialise collections of different element types - return@get call.respond(data.toJsonElement()) - } - } - } - } - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/FavouritesRoutes.kt b/api/src/main/kotlin/com/gmtkgamejam/routing/FavouritesRoutes.kt deleted file mode 100644 index d63bf4a3..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/FavouritesRoutes.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.gmtkgamejam.routing - -import com.gmtkgamejam.models.posts.dtos.FavouritePostDto -import com.gmtkgamejam.respondJSON -import com.gmtkgamejam.services.AuthService -import com.gmtkgamejam.services.FavouritesService -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Application.configureFavouritesRouting() { - - val authService = AuthService() - val favouritesService = FavouritesService() - - routing { - authenticate("auth-jwt") { - route("/favourites") { - post { - val postToFavourite = call.receive() - - val tokenSet = authService.getTokenSet(call) - ?: return@post call.respond( - status = HttpStatusCode.BadRequest, - mapOf("message" to "Failed to favourite post.") - ) - - return@post call.respond(favouritesService.addPostAsFavourite(tokenSet.discordId, postToFavourite)) - } - delete { - val postToUnFavourite = call.receive() - - authService.getTokenSet(call) - ?.let { favouritesService.getFavouritesByUserId(it.discordId) } - ?.also { it.postIds.remove(postToUnFavourite.postId) } - ?.let { favouritesService.saveFavourites(it) } - ?.let { return@delete call.respond(it) } - - call.respondJSON("Favourite couldn't be added", status = HttpStatusCode.BadRequest) - } - } - } - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt b/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt deleted file mode 100644 index ee5c6996..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/PostRoutes.kt +++ /dev/null @@ -1,331 +0,0 @@ -package com.gmtkgamejam.routing - -import com.auth0.jwt.JWT -import com.gmtkgamejam.enumFromStringSafe -import com.gmtkgamejam.models.posts.Availability -import com.gmtkgamejam.models.posts.PostItem -import com.gmtkgamejam.models.posts.Skills -import com.gmtkgamejam.models.posts.Tools -import com.gmtkgamejam.models.posts.dtos.* -import com.gmtkgamejam.repositories.PostRepository -import com.gmtkgamejam.respondJSON -import com.gmtkgamejam.services.AnalyticsService -import com.gmtkgamejam.services.AuthService -import com.gmtkgamejam.services.FavouritesService -import com.gmtkgamejam.services.PostService -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.util.* -import kotlinx.coroutines.launch -import org.bson.conversions.Bson -import org.litote.kmongo.* -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import kotlin.math.ceil -import kotlin.math.min -import kotlin.reflect.full.memberProperties -import kotlin.text.Regex.Companion.escape - -fun Application.configurePostRouting() { - - val analyticsService = AnalyticsService() - val authService = AuthService() - val service = PostService() - val favouritesService = FavouritesService() - - routing { - route("/posts") { - get { - val params = call.parameters - val page = params["page"]?.toInt() ?: 1 - val filter = and(getFilterFromParameters(params)) - - val posts = service.getPosts( - filter, - getSortFromParameters(params), - page - ) - - // Set isFavourite on posts for this user if they're logged in - call.request.header("Authorization")?.substring(7) - ?.let { JWT.decode(it) }?.getClaim("id")?.asString() - ?.let { authService.getTokenSet(it) } - ?.let { favouritesService.getFavouritesByUserId(it.discordId) } - ?.let { favouritesList -> - posts.map { it.isFavourite = favouritesList.postIds.contains(it.id) } - } - - val pagination = mapOf( - "current" to page, - "total" to ceil(service.getPostCount(filter) / PostRepository.PAGE_SIZE.toDouble()).toInt() - ) - - call.respond( - PostsDTO( - posts, - pagination - ) - ) - - launch { - analyticsService.trackQuery(params.toMap().toSortedMap()) - posts.forEach { analyticsService.trackQueryView(it) } - } - } - - get("{id}") { - val post: PostItem? = call.parameters["id"]?.let { service.getPost(it) } - if (post?.deletedAt != null) { - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - - // Set isFavourite on posts for this user if they're logged in - call.request.header("Authorization")?.substring(7) - ?.let { JWT.decode(it) }?.getClaim("id")?.asString() - ?.let { authService.getTokenSet(it) } - ?.let { favouritesService.getFavouritesByUserId(it.discordId) } - ?.let { favouritesList -> - post?.isFavourite = favouritesList.postIds.contains(post?.id) - } - - post - ?.also { launch { analyticsService.trackFullPageView(it) } } - ?.let { return@get call.respond(it) } - - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - - authenticate("auth-jwt") { - - post { - val data = call.receive() - - authService.getTokenSet(call) - ?.let { - if (service.getPostByAuthorId(it.discordId) != null) { - return@post call.respondJSON( - "Cannot have duplicate posts", - status = HttpStatusCode.BadRequest - ) - } - it - } - ?.let { - data.authorId = it.discordId // TODO: What about author name? - data.timezoneOffsets = data.timezoneOffsets.filter { tz -> tz >= -12 && tz <= 12 }.toSet() - } - ?.let { PostItem.fromCreateDto(data) } - ?.let { service.createPost(it) } - ?.let { return@post call.respond(it) } - - call.respondJSON("Post could not be created", status = HttpStatusCode.NotFound) - } - - get("favourites") { - val params = call.parameters - val page = params["page"]?.toInt() ?: 1 - - val favourites = authService.getTokenSet(call) - ?.let { favouritesService.getFavouritesByUserId(it.discordId) } - - // Exit early if the user don't have any favourites set - if (favourites!!.postIds.isEmpty()) { - return@get call.respond(emptyList()) - } - - val favouritesFilters = mutableListOf() - favourites.postIds.forEach { - favouritesFilters.add(and(PostItem::id eq it, PostItem::deletedAt eq null)) - } - - val posts = service.getPosts( - and( - or(favouritesFilters), - and(getFilterFromParameters(params)) - ), - getSortFromParameters(params), - page - ) - posts.map { post -> post.isFavourite = true } - - - val pagination = mapOf( - "current" to page, - "total" to ceil(favourites.postIds.size / PostRepository.PAGE_SIZE.toDouble()).toInt() - ) - - call.respond( - PostsDTO( - posts, - pagination - ) - ) - } - - route("/mine") { - get { - authService.getTokenSet(call) - ?.let { service.getPostByAuthorId(it.discordId) } - ?.let { return@get call.respond(it) } - - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - - put { - val data = call.receive() - - authService.getTokenSet(call) - ?.let { service.getPostByAuthorId(it.discordId) } - ?.let { post -> - // Ugly-but-functional way to update all of the fields in the DTO - data.author?.also { post.author = it } - data.description?.also { post.description = it } - data.size?.also { post.size = min(it, 20) } - data.skillsPossessed?.also { post.skillsPossessed = it } - data.skillsSought?.also { post.skillsSought = it } - data.preferredTools?.also { post.preferredTools = it } - data.languages?.also { post.languages = it } - data.languages?.also { post.languages = it } - data.availability?.also { post.availability = it } - data.timezoneOffsets?.also { post.timezoneOffsets = it.filter { tz -> tz >= -12 && tz <= 12 }.toSet() } - - post.updatedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - - service.updatePost(post) - return@put call.respond(post) - } - - // TODO: Replace BadRequest with contextual response - call.respondJSON("Could not update Post", status = HttpStatusCode.BadRequest) - } - - delete { - authService.getTokenSet(call) - ?.let { service.getPostByAuthorId(it.discordId) } - ?.let { - service.deletePost(it) - return@delete call.respondJSON("Post deleted", status = HttpStatusCode.OK) - } - - // TODO: Replace BadRequest with contextual response - call.respondJSON("Could not delete Post", status = HttpStatusCode.BadRequest) - } - } - - route("/report") - { - post { - val data = call.receive() - - service.getPost(data.id)?.let { - it.reportCount++ - service.updatePost(it) - return@post call.respond(it) - } - - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - } - - route("/report-unable-to-contact") - { - post { - val data = call.receive() - - service.getPost(data.id)?.let { - it.unableToContactCount++ - service.updatePost(it) - return@post call.respond(it) - } - - call.respondJSON("Post not found", status = HttpStatusCode.NotFound) - } - } - } - } - } -} - -fun getFilterFromParameters(params: Parameters): List { - val filters = mutableListOf(PostItem::deletedAt eq null) - - params["description"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&description=` - ?.map { it -> it.trim() } - // The regex is the easiest way to check if a description contains a given substring - ?.forEach { filters.add(PostItem::description regex escape(it).toRegex(RegexOption.IGNORE_CASE)) } - - val skillsPossessedSearchMode = params["skillsPossessedSearchMode"] ?: "and" - params["skillsPossessed"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&skillsPossessed=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::skillsPossessed contains it } - ?.let { if (skillsPossessedSearchMode == "and") and(it) else or(it) } - ?.let(filters::add) - - val skillsSoughtSearchMode = params["skillsSoughtSearchMode"] ?: "and" - params["skillsSought"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&skillsSought=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::skillsSought contains it } - ?.let { if (skillsSoughtSearchMode == "and") and(it) else or(it) } - ?.let(filters::add) - - params["tools"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&skillsSought=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::preferredTools contains it } - ?.let(filters::addAll) - - params["languages"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&languages=` - ?.map { PostItem::languages contains it } - ?.let { filters.add(or(it)) } - - params["availability"]?.split(',') - ?.filter(String::isNotBlank) // Filter out empty `&availability=` - ?.mapNotNull { enumFromStringSafe(it) } - ?.map { PostItem::availability eq it } - // Availabilities are mutually exclusive, so treat it as inclusion search - ?.let { filters.add(or(it)) } - - // If no timezones sent, lack of filters will search all timezones - if (params["timezoneStart"] != null && params["timezoneEnd"] != null) { - val timezoneStart: Int = params["timezoneStart"]!!.toInt() - val timezoneEnd: Int = params["timezoneEnd"]!!.toInt() - - val timezones: MutableList = mutableListOf() - if (timezoneStart == timezoneEnd) { - timezones.add(timezoneStart) - } else if (timezoneStart < timezoneEnd) { - // UTC-2 -> UTC+2 should be: [-2, -1, 0, 1, 2] - timezones.addAll((timezoneStart..timezoneEnd)) - } else { - // UTC+9 -> UTC-9 should be: [9, 10, 11, 12, -12, -11, -10, -9] - timezones.addAll((timezoneStart..12)) - timezones.addAll((-12..timezoneEnd)) - } - - // Add all timezone searches as eq checks - // It's brute force, but easier to confirm - timezones - .map { PostItem::timezoneOffsets contains it } - .let { filters.add(or(it)) } - } - - return filters -} - -fun getSortFromParameters(params: Parameters): Bson { - val sortByFieldName = params["sortBy"] ?: "createdAt" - val sortByField = PostItem::class.memberProperties.first { prop -> prop.name == sortByFieldName } - return when (params["sortDir"].toString()) { - "asc" -> ascending(sortByField) - "desc" -> descending(sortByField) - else -> descending(sortByField) - } -} diff --git a/api/src/main/kotlin/com/gmtkgamejam/services/AdminService.kt b/api/src/main/kotlin/com/gmtkgamejam/services/AdminService.kt deleted file mode 100644 index 429ae95c..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/services/AdminService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.gmtkgamejam.services - -import com.gmtkgamejam.models.admin.BannedUser -import com.gmtkgamejam.repositories.AdminRepository -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class AdminService : KoinComponent { - private val repository: AdminRepository by inject() - - fun banUser(bannedUser: BannedUser) { - repository.banUser(bannedUser) - } - - fun unbanUser(bannedUser: BannedUser) { - repository.unbanUser(bannedUser) - } -} - diff --git a/api/src/main/kotlin/com/gmtkgamejam/services/AnalyticsService.kt b/api/src/main/kotlin/com/gmtkgamejam/services/AnalyticsService.kt deleted file mode 100644 index 5fc518ad..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/services/AnalyticsService.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.gmtkgamejam.services - -import com.gmtkgamejam.models.posts.PostItem -import com.gmtkgamejam.repositories.AnalyticsRepository -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class AnalyticsService : KoinComponent { - private val postService = PostService() - private val repository: AnalyticsRepository by inject() - - fun trackQueryView(post: PostItem) { - postService.addQueryView(post) - } - - fun trackFullPageView(post: PostItem) { - postService.addFullPageView(post) - } - - fun trackQuery(queryParams: Map) { - repository.trackQuery(queryParams) - } - - fun trackLogin() { - repository.trackLogin() - } -} - diff --git a/api/src/main/kotlin/com/gmtkgamejam/services/FavouritesService.kt b/api/src/main/kotlin/com/gmtkgamejam/services/FavouritesService.kt deleted file mode 100644 index 5f6eec15..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/services/FavouritesService.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.gmtkgamejam.services - -import com.gmtkgamejam.models.posts.dtos.FavouritePostDto -import com.gmtkgamejam.models.posts.FavouritesList -import com.gmtkgamejam.repositories.FavouritesRepository -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class FavouritesService : KoinComponent { - private val repository: FavouritesRepository by inject() - - fun getFavouritesByUserId(discordId: String): FavouritesList { - return repository.getFavouritesByUserId(discordId) ?: FavouritesList(discordId) - } - - fun saveFavourites(favourites: FavouritesList): FavouritesList { - repository.saveFavourites(favourites) - return favourites - } - - fun addPostAsFavourite(discordId: String, post: FavouritePostDto) = - this.saveFavourites((repository.getFavouritesByUserId(discordId) ?: FavouritesList(discordId)) - .also { - it.postIds.add(post.postId) - } - ) -} - diff --git a/api/src/main/kotlin/com/gmtkgamejam/services/PostService.kt b/api/src/main/kotlin/com/gmtkgamejam/services/PostService.kt deleted file mode 100644 index a5fa58ef..00000000 --- a/api/src/main/kotlin/com/gmtkgamejam/services/PostService.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.gmtkgamejam.services - -import com.gmtkgamejam.models.posts.PostItem -import com.gmtkgamejam.repositories.PostRepository -import org.bson.conversions.Bson -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class PostService : KoinComponent { - - private val repository: PostRepository by inject() - - fun createPost(postItem: PostItem) { - repository.createPost(postItem) - } - - fun getPosts(filter: Bson, sort: Bson, page: Int): List { - return repository.getPosts(filter, sort, page) - } - - fun getPost(id: String) : PostItem? { - return repository.getPost(id) - } - - fun getPostByAuthorId(authorId: String, ignoreDeletion: Boolean = false) : PostItem? { - return repository.getPostByAuthorId(authorId, ignoreDeletion) - } - - fun getPostCount(filter: Bson): Int { - return repository.getPostCount(filter) - } - - fun updatePost(postItem: PostItem) { - repository.updatePost(postItem) - } - - fun deletePost(postItem: PostItem) { - repository.deletePost(postItem) - } - - fun addQueryView(postItem: PostItem) { - repository.addQueryView(postItem) - } - - fun addFullPageView(postItem: PostItem) { - repository.addFullPageView(postItem) - } - -} - diff --git a/api/src/main/kotlin/com/gmtkgamejam/Application.kt b/api/src/main/kotlin/com/gmtkvotingsystem/Application.kt similarity index 66% rename from api/src/main/kotlin/com/gmtkgamejam/Application.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/Application.kt index f7f66cf6..3bb1321f 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/Application.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/Application.kt @@ -1,19 +1,22 @@ -package com.gmtkgamejam +@file:Suppress("ktlint:standard:no-wildcard-imports") -import com.gmtkgamejam.koin.DatabaseModule -import com.gmtkgamejam.koin.DiscordBotModule -import com.gmtkgamejam.routing.* +package com.gmtkvotingsystem + +import com.gmtkvotingsystem.koin.DatabaseModule +import com.gmtkvotingsystem.koin.DiscordBotModule +import com.gmtkvotingsystem.routing.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.plugins.cors.* import io.ktor.server.plugins.cors.routing.CORS import kotlinx.serialization.json.Json import org.koin.core.context.startKoin import org.koin.environmentProperties -fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) +fun main(args: Array): Unit = + io.ktor.server.netty.EngineMain + .main(args) @Suppress("unused") fun Application.module() { @@ -25,23 +28,22 @@ fun Application.module() { configureRequestHandling() configureUserInfoRouting() configureAuthRouting() - configureAdminRouting() - configurePostRouting() - configureFavouritesRouting() - configureDiscordBotRouting() + configureThemeRouting() + configureVoteRouting() } fun Application.configureRequestHandling() { install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }, + ) } - install(CORS) - { + install(CORS) { anyHost() allowMethod(HttpMethod.Options) diff --git a/api/src/main/kotlin/com/gmtkgamejam/ApplicationResponseExtensions.kt b/api/src/main/kotlin/com/gmtkvotingsystem/ApplicationResponseExtensions.kt similarity index 91% rename from api/src/main/kotlin/com/gmtkgamejam/ApplicationResponseExtensions.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/ApplicationResponseExtensions.kt index 53b993a3..8ad1a2b7 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/ApplicationResponseExtensions.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/ApplicationResponseExtensions.kt @@ -1,4 +1,4 @@ -package com.gmtkgamejam +package com.gmtkvotingsystem import io.ktor.http.* import io.ktor.server.application.* diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/AuthFromRequest.kt b/api/src/main/kotlin/com/gmtkvotingsystem/AuthFromRequest.kt new file mode 100644 index 00000000..4c4d5813 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/AuthFromRequest.kt @@ -0,0 +1,21 @@ +package com.gmtkvotingsystem + +import com.auth0.jwt.JWT +import com.gmtkvotingsystem.models.auth.AuthTokenSet +import com.gmtkvotingsystem.services.AuthService +import io.ktor.server.application.* +import io.ktor.server.request.* + +public fun getAuthFromCall( + authService: AuthService, + call: ApplicationCall, +): AuthTokenSet? { + // Set isFavourite on posts for this user if they're logged in + return call.request + .header("Authorization") + ?.substring(7) + ?.let { JWT.decode(it) } + ?.getClaim("id") + ?.asString() + ?.let { authService.getTokenSet(it) } +} diff --git a/api/src/main/kotlin/com/gmtkgamejam/AuthModule.kt b/api/src/main/kotlin/com/gmtkvotingsystem/AuthModule.kt similarity index 95% rename from api/src/main/kotlin/com/gmtkgamejam/AuthModule.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/AuthModule.kt index 6d433ff5..9a16f74c 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/AuthModule.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/AuthModule.kt @@ -1,10 +1,10 @@ -package com.gmtkgamejam +package com.gmtkvotingsystem import com.auth0.jwt.JWT import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm -import com.gmtkgamejam.discord.discordHttpClient -import com.gmtkgamejam.services.AuthService +import com.gmtkvotingsystem.discord.discordHttpClient +import com.gmtkvotingsystem.services.AuthService import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -46,7 +46,7 @@ fun Application.authModule() { requestMethod = HttpMethod.Post, clientId = Config.getString("secrets.discord.client.id"), clientSecret = Config.getString("secrets.discord.client.secret"), - defaultScopes = listOf("identify", "guilds.members.read") + defaultScopes = listOf("identify", "guilds.members.read"), ) } } diff --git a/api/src/main/kotlin/com/gmtkgamejam/CollectionExtensions.kt b/api/src/main/kotlin/com/gmtkvotingsystem/CollectionExtensions.kt similarity index 98% rename from api/src/main/kotlin/com/gmtkgamejam/CollectionExtensions.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/CollectionExtensions.kt index 4192407a..c2e2307a 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/CollectionExtensions.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/CollectionExtensions.kt @@ -1,4 +1,4 @@ -package com.gmtkgamejam +package com.gmtkvotingsystem import kotlinx.serialization.json.* import java.time.LocalDateTime @@ -37,4 +37,4 @@ fun Map<*, *>.toJsonElement(): JsonElement { } } return JsonObject(map) -} \ No newline at end of file +} diff --git a/api/src/main/kotlin/com/gmtkgamejam/Config.kt b/api/src/main/kotlin/com/gmtkvotingsystem/Config.kt similarity index 95% rename from api/src/main/kotlin/com/gmtkgamejam/Config.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/Config.kt index 41284d85..5ccbaa62 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/Config.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/Config.kt @@ -1,4 +1,4 @@ -package com.gmtkgamejam +package com.gmtkvotingsystem import io.ktor.server.config.* diff --git a/api/src/main/kotlin/com/gmtkgamejam/EnumExtensions.kt b/api/src/main/kotlin/com/gmtkvotingsystem/EnumExtensions.kt similarity index 66% rename from api/src/main/kotlin/com/gmtkgamejam/EnumExtensions.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/EnumExtensions.kt index cc1ac2dd..276285c3 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/EnumExtensions.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/EnumExtensions.kt @@ -1,10 +1,8 @@ -package com.gmtkgamejam +package com.gmtkvotingsystem /** * Floating function to cast a String to an Enum without throwing an exception * * Suggest using with mapNotNull{} where possible */ -inline fun > enumFromStringSafe(value: String) : A? { - return enumValues().find { s -> s.name == value.uppercase() } -} \ No newline at end of file +inline fun > enumFromStringSafe(value: String): A? = enumValues().find { s -> s.name == value.uppercase() } diff --git a/api/src/main/kotlin/com/gmtkgamejam/bot/DiscordBot.kt b/api/src/main/kotlin/com/gmtkvotingsystem/bot/DiscordBot.kt similarity index 50% rename from api/src/main/kotlin/com/gmtkgamejam/bot/DiscordBot.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/bot/DiscordBot.kt index ed9ba632..80fb130f 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/bot/DiscordBot.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/bot/DiscordBot.kt @@ -1,12 +1,9 @@ -package com.gmtkgamejam.bot +package com.gmtkvotingsystem.bot -import com.gmtkgamejam.Config -import kotlinx.coroutines.Dispatchers +import com.gmtkvotingsystem.Config import kotlinx.coroutines.future.await -import kotlinx.coroutines.withContext import org.javacord.api.DiscordApi import org.javacord.api.DiscordApiBuilder -import org.javacord.api.entity.channel.ServerTextChannel import org.javacord.api.entity.intent.Intent import org.javacord.api.entity.message.MessageBuilder import org.javacord.api.entity.server.Server @@ -15,7 +12,6 @@ import org.javacord.api.exception.DiscordException import org.javacord.api.exception.MissingPermissionsException import org.slf4j.Logger import org.slf4j.LoggerFactory -import kotlin.jvm.optionals.getOrElse class DiscordBot { private val logger: Logger = LoggerFactory.getLogger(javaClass) @@ -24,70 +20,25 @@ class DiscordBot { private lateinit var server: Server - private lateinit var channel: ServerTextChannel - - private val messageBuilder = BotMessageBuilder() - private val approvedUsers: MutableList = mutableListOf() init { val token = Config.getString("bot.token") val builder = DiscordApiBuilder().setToken(token).setIntents(Intent.GUILD_MEMBERS) - val guildId = Config.getString("jam.guildId") - val channelName = Config.getString("bot.pingChannel") + val guildId = Config.getString("bot.guildId") try { api = builder.login().join() server = api.getServerById(guildId).get() - channel = api.getServerTextChannelsByNameIgnoreCase(channelName).first() - - api.setMessageCacheSize(0, 0) - channel.messageCache.capacity = 0 - channel.messageCache.storageTimeInSeconds = 0 - logger.info("Discord bot is online and ready for action!") - } catch (ex: NoSuchElementException) { // NoSuchElementException triggered by calling `.first()` on Collection - logger.warn("Discord bot could not connect to pingChannel [$channelName] - ping message integration offline.") } catch (ex: Exception) { logger.warn("Discord bot could not be initialised - continuing...") logger.warn(ex.toString()) } } - suspend fun createContactUserPingMessage(recipientUserId: String, senderUserId: String) { - val recipient: User = api.getUserById(recipientUserId).await() - val sender: User = api.getUserById(senderUserId).await() - - val dmChannel = recipient.privateChannel.getOrElse { recipient.openPrivateChannel().get() } - - val messageSendAttempt = if (messageBuilder.canBuildEmbedFromUser(sender)) { - logger.info("[CONTACT] [EMBED] ${sender.name} ($senderUserId) contacted ${recipient.name} ($recipientUserId)") - dmChannel.sendMessage(messageBuilder.embedMessage(recipient, sender)) - } else { - logger.info("[CONTACT] [BASIC] ${sender.name} ($senderUserId) contacted ${recipient.name} ($recipientUserId)") - dmChannel.sendMessage(messageBuilder.basicMessage(recipient, sender)) - } - - try { - withContext(Dispatchers.IO) { - messageSendAttempt.get() - } - } catch (ex: InterruptedException) { - createFallbackChannelPingMessage(recipient, sender) - } catch (ex: java.util.concurrent.ExecutionException) { - createFallbackChannelPingMessage(recipient, sender) - } - } - - private suspend fun createFallbackChannelPingMessage(recipient: User, sender: User) { - val messageContents = "Hey ${recipient.mentionTag} (${recipient.name}), ${sender.mentionTag} wants to get in contact about your Team Finder post!" - // TODO: Validate message actually sent, give error otherwise - logger.info("[CONTACT] [PING] ${sender.name} (${sender.id}) contacted ${recipient.name} (${recipient.id})") - channel.sendMessage(messageContents).await() - } - suspend fun doesUserHaveValidPermissions(userId: String): Boolean { // If the API hasn't authenticated, we either have technical issues, or the token wasn't set properly // (e.g. running locally). If so, just run like normal for now. @@ -106,15 +57,16 @@ class DiscordBot { private suspend fun trySendMessage(userId: String): Boolean { var didMessageFailBecausePerms = false - try { + try { val user: User = api.getUserById(userId).await() logger.debug("Trying to send user garbage message...") - val messageResult = MessageBuilder() - .append("") // Intentionally left blank! - .send(user) - .await() + val messageResult = + MessageBuilder() + .append("") // Intentionally left blank! + .send(user) + .await() logger.error("This shouldn't print, but you never know - message: $messageResult") } catch (ex: MissingPermissionsException) { @@ -131,15 +83,17 @@ class DiscordBot { return !didMessageFailBecausePerms } - fun isUserInGuild(userId: String): Boolean { - return server.getMemberById(userId).isPresent - } + fun isUserInGuild(userId: String): Boolean = server.getMemberById(userId).isPresent fun getDisplayNameForUser(userId: String): String { val baseUserName = api.getUserById(userId).get().name return try { - server.getMemberById(userId).get().getNickname(server).get() + server + .getMemberById(userId) + .get() + .getNickname(server) + .get() } catch (ex: NoSuchElementException) { baseUserName } diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/discord/HttpClient.kt b/api/src/main/kotlin/com/gmtkvotingsystem/discord/HttpClient.kt new file mode 100644 index 00000000..19383b0b --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/discord/HttpClient.kt @@ -0,0 +1,20 @@ +package com.gmtkvotingsystem.discord + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json + +fun discordHttpClient(): HttpClient = + HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + isLenient = true + prettyPrint = true + }, + ) + } + } diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/discord/RefreshToken.kt b/api/src/main/kotlin/com/gmtkvotingsystem/discord/RefreshToken.kt new file mode 100644 index 00000000..9c2cd8b6 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/discord/RefreshToken.kt @@ -0,0 +1,30 @@ +package com.gmtkvotingsystem.discord + +import com.gmtkvotingsystem.models.auth.DiscordRefreshTokenResponse +import io.ktor.client.call.body +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* + +const val refreshTokenEndpoint = "https://discord.com/api/oauth2/token" + +suspend fun refreshTokenAsync( + clientId: String, + clientSecret: String, + refreshToken: String, +): DiscordRefreshTokenResponse = + discordHttpClient().use { client -> + client + .post(refreshTokenEndpoint) { + setBody( + FormDataContent( + Parameters.build { + append("client_id", clientId) + append("client_secret", clientSecret) + append("grant_type", "refresh_token") + append("refresh_token", refreshToken) + }, + ), + ) + }.body() + } diff --git a/api/src/main/kotlin/com/gmtkgamejam/discord/UserInfo.kt b/api/src/main/kotlin/com/gmtkvotingsystem/discord/UserInfo.kt similarity index 72% rename from api/src/main/kotlin/com/gmtkgamejam/discord/UserInfo.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/discord/UserInfo.kt index dd4c12b8..910ce214 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/discord/UserInfo.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/discord/UserInfo.kt @@ -1,21 +1,19 @@ -package com.gmtkgamejam.discord +package com.gmtkvotingsystem.discord -import com.gmtkgamejam.models.auth.DiscordUserInfo +import com.gmtkvotingsystem.models.auth.DiscordUserInfo import io.ktor.client.call.body import io.ktor.client.request.* import io.ktor.http.* const val userInfo = "https://discordapp.com/api/users/@me" -suspend fun getUserInfoAsync(accessToken: String): DiscordUserInfo { - return discordHttpClient().use { client -> +suspend fun getUserInfoAsync(accessToken: String): DiscordUserInfo = + discordHttpClient().use { client -> client .get(userInfo) { headers { append(HttpHeaders.Accept, "application/json") append(HttpHeaders.Authorization, "Bearer $accessToken") } - } - .body() + }.body() } -} diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/koin/DatabaseModule.kt b/api/src/main/kotlin/com/gmtkvotingsystem/koin/DatabaseModule.kt new file mode 100644 index 00000000..90d9c801 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/koin/DatabaseModule.kt @@ -0,0 +1,13 @@ +package com.gmtkvotingsystem.koin + +import com.gmtkvotingsystem.Config +import org.koin.dsl.module +import org.litote.kmongo.KMongo + +val DatabaseModule = + module(createdAtStart = true) { + single { + val url = Config.getString("secrets.database.url") + KMongo.createClient(url) + } + } diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/koin/DiscordBotModule.kt b/api/src/main/kotlin/com/gmtkvotingsystem/koin/DiscordBotModule.kt new file mode 100644 index 00000000..d29e6a67 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/koin/DiscordBotModule.kt @@ -0,0 +1,11 @@ +package com.gmtkvotingsystem.koin + +import com.gmtkvotingsystem.bot.DiscordBot +import org.koin.dsl.module + +val DiscordBotModule = + module(createdAtStart = true) { + single { + DiscordBot() + } + } diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/models/Theme.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/Theme.kt new file mode 100644 index 00000000..e926067b --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/Theme.kt @@ -0,0 +1,13 @@ +package com.gmtkvotingsystem.models + +import kotlinx.serialization.Serializable +import java.util.UUID + +@Serializable +data class Theme( + val _id: String, + val discordId: String, + val text: String, +) { + constructor(discordId: String, text: String) : this(UUID.randomUUID().toString(), discordId, text) +} diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeDTO.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeDTO.kt new file mode 100644 index 00000000..efb5f23b --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeDTO.kt @@ -0,0 +1,8 @@ +package com.gmtkvotingsystem.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ThemeDTO( + val themes: List, +) diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeVotedOnDTO.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeVotedOnDTO.kt new file mode 100644 index 00000000..89817dcf --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/ThemeVotedOnDTO.kt @@ -0,0 +1,11 @@ +package com.gmtkvotingsystem.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ThemeVotedOnDTO( + val _id: String, + val discordId: String, + val text: String, + var score: Int, +) diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/models/Vote.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/Vote.kt new file mode 100644 index 00000000..a5634526 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/Vote.kt @@ -0,0 +1,12 @@ +package com.gmtkvotingsystem.models + +import java.util.* + +data class Vote( + val _id: String, + val themeId: String, + val discordId: String, + val score: Int, +) { + constructor(themeId: String, discordId: String, score: Int) : this(UUID.randomUUID().toString(), themeId, discordId, score) +} diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/models/VoteDTO.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/VoteDTO.kt new file mode 100644 index 00000000..09c969d9 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/VoteDTO.kt @@ -0,0 +1,9 @@ +package com.gmtkvotingsystem.models + +import kotlinx.serialization.Serializable + +@Serializable +data class VoteDTO( + val id: String, + val score: Int, +) diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/models/VotesDTO.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/VotesDTO.kt new file mode 100644 index 00000000..8570c9bc --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/VotesDTO.kt @@ -0,0 +1,8 @@ +package com.gmtkvotingsystem.models + +import kotlinx.serialization.Serializable + +@Serializable +data class VotesDTO( + val votes: List, +) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/auth/AuthTokenSet.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/AuthTokenSet.kt similarity index 92% rename from api/src/main/kotlin/com/gmtkgamejam/models/auth/AuthTokenSet.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/models/auth/AuthTokenSet.kt index 87867409..e2b50e56 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/models/auth/AuthTokenSet.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/AuthTokenSet.kt @@ -1,4 +1,4 @@ -package com.gmtkgamejam.models.auth +package com.gmtkvotingsystem.models.auth import java.util.* diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/auth/DiscordRefreshTokenResponse.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/DiscordRefreshTokenResponse.kt similarity index 84% rename from api/src/main/kotlin/com/gmtkgamejam/models/auth/DiscordRefreshTokenResponse.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/models/auth/DiscordRefreshTokenResponse.kt index 06c8e546..476e107e 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/models/auth/DiscordRefreshTokenResponse.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/DiscordRefreshTokenResponse.kt @@ -1,4 +1,4 @@ -package com.gmtkgamejam.models.auth +package com.gmtkvotingsystem.models.auth import kotlinx.serialization.Serializable @@ -9,4 +9,4 @@ data class DiscordRefreshTokenResponse( val expires_in: Long, val refresh_token: String, val scope: String, -) \ No newline at end of file +) diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/auth/DiscordUserInfo.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/DiscordUserInfo.kt similarity index 84% rename from api/src/main/kotlin/com/gmtkgamejam/models/auth/DiscordUserInfo.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/models/auth/DiscordUserInfo.kt index 88d6cb6e..0961cc97 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/models/auth/DiscordUserInfo.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/DiscordUserInfo.kt @@ -1,4 +1,4 @@ -package com.gmtkgamejam.models.auth +package com.gmtkvotingsystem.models.auth import kotlinx.serialization.Serializable diff --git a/api/src/main/kotlin/com/gmtkgamejam/models/auth/UserInfo.kt b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/UserInfo.kt similarity index 71% rename from api/src/main/kotlin/com/gmtkgamejam/models/auth/UserInfo.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/models/auth/UserInfo.kt index a897c704..8c345d06 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/models/auth/UserInfo.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/models/auth/UserInfo.kt @@ -1,9 +1,9 @@ -package com.gmtkgamejam.models.auth +package com.gmtkvotingsystem.models.auth -import com.gmtkgamejam.Config +import com.gmtkvotingsystem.Config import kotlinx.serialization.Serializable -private val adminIds: List = Config.getList("jam.adminIds") +private val adminIds: List = Config.getList("bot.adminIds") @Serializable data class UserInfo( @@ -12,15 +12,14 @@ data class UserInfo( val avatar: String, val isInDiscordServer: Boolean, val hasContactPermsSet: Boolean, - val isAdmin: Boolean + val isAdmin: Boolean, ) { - constructor(discordUserInfo: DiscordUserInfo, displayName: String, isInDiscordServer: Boolean, hasContactPermsSet: Boolean) : this( discordUserInfo.id, displayName, discordUserInfo.avatar ?: "no-avatar", isInDiscordServer, hasContactPermsSet, - adminIds.contains(discordUserInfo.id) + adminIds.contains(discordUserInfo.id), ) } diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/AuthRoutes.kt b/api/src/main/kotlin/com/gmtkvotingsystem/routing/AuthRoutes.kt similarity index 60% rename from api/src/main/kotlin/com/gmtkgamejam/routing/AuthRoutes.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/routing/AuthRoutes.kt index ce1265e2..d6e323da 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/AuthRoutes.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/routing/AuthRoutes.kt @@ -1,11 +1,11 @@ -package com.gmtkgamejam.routing +package com.gmtkvotingsystem.routing import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import com.gmtkgamejam.Config -import com.gmtkgamejam.discord.getUserInfoAsync -import com.gmtkgamejam.models.auth.AuthTokenSet -import com.gmtkgamejam.services.AuthService +import com.gmtkvotingsystem.Config +import com.gmtkvotingsystem.discord.getUserInfoAsync +import com.gmtkvotingsystem.models.auth.AuthTokenSet +import com.gmtkvotingsystem.services.AuthService import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.response.* @@ -14,7 +14,6 @@ import java.security.SecureRandom import java.util.* fun Application.configureAuthRouting() { - val service = AuthService() routing { @@ -27,28 +26,31 @@ fun Application.configureAuthRouting() { val issuer = Config.getString("jwt.issuer") val audience = Config.getString("jwt.audience") - val lifespanOfAppJwt = 86400000 // A user is logged into the Team Finder for 24 hours + val lifespanOfAppJwt = 86400000 // A securely random ID is used to ensure a JWT is unique, and that another JWT can't be brute-forced val randomId = getSecureId() - val token = JWT.create() - .withAudience(audience) - .withIssuer(issuer) - .withClaim("id", randomId) - .withExpiresAt(Date(System.currentTimeMillis() + lifespanOfAppJwt)) - .sign(Algorithm.HMAC256(secret)) + val token = + JWT + .create() + .withAudience(audience) + .withIssuer(issuer) + .withClaim("id", randomId) + .withExpiresAt(Date(System.currentTimeMillis() + lifespanOfAppJwt)) + .sign(Algorithm.HMAC256(secret)) call.principal()?.let { val user = getUserInfoAsync(it.accessToken) - val tokenSet = AuthTokenSet( - randomId, - user.id, - it.accessToken, - it.tokenType, - Date(System.currentTimeMillis() + it.expiresIn), - it.refreshToken - ) + val tokenSet = + AuthTokenSet( + randomId, + user.id, + it.accessToken, + it.tokenType, + Date(System.currentTimeMillis() + it.expiresIn), + it.refreshToken, + ) service.storeTokenSet(tokenSet) val redirectTarget = Config.getString("ui.host") diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/routing/ThemeRoutes.kt b/api/src/main/kotlin/com/gmtkvotingsystem/routing/ThemeRoutes.kt new file mode 100644 index 00000000..c2e2758b --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/routing/ThemeRoutes.kt @@ -0,0 +1,37 @@ +package com.gmtkvotingsystem.routing + +import com.gmtkvotingsystem.getAuthFromCall +import com.gmtkvotingsystem.models.ThemeDTO +import com.gmtkvotingsystem.services.AuthService +import com.gmtkvotingsystem.services.ThemeService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureThemeRouting() { + val authService = AuthService() + val themeService = ThemeService() + + routing { + authenticate("auth-jwt") { + get("/themes") { + val auth = getAuthFromCall(authService, call) ?: return@get call.respond(HttpStatusCode.BadRequest) + + call.respond(themeService.getForUser(auth.discordId)) + } + + post("/themes") { + val auth = getAuthFromCall(authService, call) ?: return@post call.respond(HttpStatusCode.BadRequest) + val data = call.receive() + val themes = data.themes + + themeService.setForUser(auth.discordId, themes) + + call.respond(themeService.getForUser(auth.discordId)) + } + } + } +} diff --git a/api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt b/api/src/main/kotlin/com/gmtkvotingsystem/routing/UserInfoRoutes.kt similarity index 67% rename from api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/routing/UserInfoRoutes.kt index 9150476b..0b296634 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/routing/UserInfoRoutes.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/routing/UserInfoRoutes.kt @@ -1,31 +1,30 @@ -package com.gmtkgamejam.routing +@file:Suppress("ktlint:standard:no-wildcard-imports") -import com.gmtkgamejam.Config -import com.gmtkgamejam.bot.DiscordBot -import com.gmtkgamejam.discord.getUserInfoAsync -import com.gmtkgamejam.discord.refreshTokenAsync -import com.gmtkgamejam.models.auth.UserInfo -import com.gmtkgamejam.respondJSON -import com.gmtkgamejam.services.AnalyticsService -import com.gmtkgamejam.services.AuthService +package com.gmtkvotingsystem.routing + +import com.gmtkvotingsystem.Config +import com.gmtkvotingsystem.bot.DiscordBot +import com.gmtkvotingsystem.discord.getUserInfoAsync +import com.gmtkvotingsystem.discord.refreshTokenAsync +import com.gmtkvotingsystem.models.auth.UserInfo +import com.gmtkvotingsystem.respondJSON +import com.gmtkvotingsystem.services.AuthService import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* import io.ktor.server.response.* import io.ktor.server.routing.* -import kotlinx.coroutines.launch import org.koin.ktor.ext.inject import java.util.* fun Application.configureUserInfoRouting() { val bot: DiscordBot by inject() - - val analyticsService = AnalyticsService() val service = AuthService() routing { - authenticate("auth-jwt") { // These routes go through the authentication middleware defined in Auth.kt + authenticate("auth-jwt") { + // These routes go through the authentication middleware defined in Auth.kt get("/hello") { val principal = call.principal() val id = principal?.payload?.getClaim("id")?.asString() @@ -45,11 +44,12 @@ fun Application.configureUserInfoRouting() { // If access token has expired, try a dirty inline refresh val tokenHasExpired = tokenSet.expiry <= Date(System.currentTimeMillis()) if (tokenHasExpired) { - val refreshedTokenSet = refreshTokenAsync( - Config.getString("secrets.discord.client.id"), - Config.getString("secrets.discord.client.secret"), - it.refreshToken.toString() - ) + val refreshedTokenSet = + refreshTokenAsync( + Config.getString("secrets.discord.client.id"), + Config.getString("secrets.discord.client.secret"), + it.refreshToken.toString(), + ) tokenSet.refresh(refreshedTokenSet) service.updateTokenSet(tokenSet) @@ -59,10 +59,6 @@ fun Application.configureUserInfoRouting() { val user = getUserInfoAsync(accessToken) - launch { - analyticsService.trackLogin() - } - val displayName = bot.getDisplayNameForUser(user.id) val hasPermissions = bot.doesUserHaveValidPermissions(user.id) val isUserInGuild = bot.isUserInGuild(user.id) diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/routing/VoteRoutes.kt b/api/src/main/kotlin/com/gmtkvotingsystem/routing/VoteRoutes.kt new file mode 100644 index 00000000..e31cc01c --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/routing/VoteRoutes.kt @@ -0,0 +1,40 @@ +package com.gmtkvotingsystem.routing + +import com.gmtkvotingsystem.getAuthFromCall +import com.gmtkvotingsystem.models.VotesDTO +import com.gmtkvotingsystem.services.AuthService +import com.gmtkvotingsystem.services.ThemeService +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureVoteRouting() { + val authService = AuthService() + val themeService = ThemeService() + + routing { + authenticate("auth-jwt") { + get("/votes") { + val auth = getAuthFromCall(authService, call) ?: return@get call.respond(HttpStatusCode.BadRequest) + + call.respond(mapOf("votes" to themeService.getAsVotesForUser(auth.discordId))) + } + + post("/votes") { + val auth = getAuthFromCall(authService, call) ?: return@post call.respond(HttpStatusCode.BadRequest) + val data = call.receive() + + if (data.votes.any { it.score < -2 || it.score > 2 }) { + return@post call.respond(HttpStatusCode.BadRequest) + } + + themeService.setAsVotesForUser(auth.discordId, data) + + call.respond(themeService.getForUser(auth.discordId)) + } + } + } +} diff --git a/api/src/main/kotlin/com/gmtkgamejam/services/AuthService.kt b/api/src/main/kotlin/com/gmtkvotingsystem/services/AuthService.kt similarity index 79% rename from api/src/main/kotlin/com/gmtkgamejam/services/AuthService.kt rename to api/src/main/kotlin/com/gmtkvotingsystem/services/AuthService.kt index 5c5d3010..ea3a97d8 100644 --- a/api/src/main/kotlin/com/gmtkgamejam/services/AuthService.kt +++ b/api/src/main/kotlin/com/gmtkvotingsystem/services/AuthService.kt @@ -1,6 +1,8 @@ -package com.gmtkgamejam.services +@file:Suppress("ktlint:standard:no-wildcard-imports") -import com.gmtkgamejam.models.auth.AuthTokenSet +package com.gmtkvotingsystem.services + +import com.gmtkvotingsystem.models.auth.AuthTokenSet import com.mongodb.client.MongoClient import com.mongodb.client.MongoCollection import com.mongodb.client.model.UpdateOptions @@ -15,13 +17,12 @@ import org.litote.kmongo.getCollectionOfName import org.litote.kmongo.updateOne class AuthService : KoinComponent { - private val client: MongoClient by inject() private val col: MongoCollection init { - val database = client.getDatabase("team-finder") + val database = client.getDatabase("gmtk-voting-system") col = database.getCollectionOfName("auth") } @@ -29,9 +30,7 @@ class AuthService : KoinComponent { col.updateOne(AuthTokenSet::discordId eq tokenSet.discordId, tokenSet, UpdateOptions().upsert(true)) } - fun getTokenSet(id: String): AuthTokenSet? { - return col.findOne(AuthTokenSet::id eq id) - } + fun getTokenSet(id: String): AuthTokenSet? = col.findOne(AuthTokenSet::id eq id) fun getTokenSet(call: ApplicationCall): AuthTokenSet? { val principal = call.principal()!! @@ -43,6 +42,4 @@ class AuthService : KoinComponent { fun updateTokenSet(tokenSet: AuthTokenSet) { col.updateOne(AuthTokenSet::id eq tokenSet.id, tokenSet) } - } - diff --git a/api/src/main/kotlin/com/gmtkvotingsystem/services/ThemeService.kt b/api/src/main/kotlin/com/gmtkvotingsystem/services/ThemeService.kt new file mode 100644 index 00000000..aa51b5e5 --- /dev/null +++ b/api/src/main/kotlin/com/gmtkvotingsystem/services/ThemeService.kt @@ -0,0 +1,61 @@ +package com.gmtkvotingsystem.services + +import com.gmtkvotingsystem.models.* +import com.mongodb.client.MongoClient +import com.mongodb.client.MongoCollection +import com.mongodb.client.model.UpdateOptions +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.litote.kmongo.eq +import org.litote.kmongo.getCollectionOfName +import org.litote.kmongo.updateOne +import kotlin.random.Random + +class ThemeService : KoinComponent { + private val client: MongoClient by inject() + + private val col: MongoCollection + private val votesCol: MongoCollection + + init { + val database = client.getDatabase("gmtk-voting-system") + col = database.getCollectionOfName("themes") + votesCol = database.getCollectionOfName("votes") + } + + fun getForUser(discordId: String): ThemeDTO { + val themes = col.find(Theme::discordId eq discordId).toList() + val themeTextList = themes.map { it.text } + return ThemeDTO(themeTextList) + } + + fun setForUser( + discordId: String, + themes: List, + ) { + col.deleteMany(Theme::discordId eq discordId) + themes.map { col.updateOne(Theme(discordId, it), UpdateOptions().upsert(true)) } + } + + fun getAsVotesForUser(discordId: String): List { + val rng = Random(discordId.toLong()) + val currentVotes = votesCol.find(Vote::discordId eq discordId).toList().associate { it.themeId to it.score } + val themes = + col + .find() + .toList() + .shuffled(rng) + .subList(0, 3) // todo + .map { theme -> ThemeVotedOnDTO(theme._id, discordId, theme.text, currentVotes[theme._id] ?: 0) } + + return themes + } + + fun setAsVotesForUser( + discordId: String, + dto: VotesDTO, + ) { + votesCol.deleteMany(Vote::discordId eq discordId) + dto.votes.map { votesCol.updateOne(Vote(it.id, discordId, it.score), UpdateOptions().upsert(true)) } + } +}