Skip to content

Commit

Permalink
Add possibility to have watchlist for a user
Browse files Browse the repository at this point in the history
  • Loading branch information
Ziedelth committed May 5, 2024
1 parent 576ed80 commit a62b3ac
Show file tree
Hide file tree
Showing 31 changed files with 1,336 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package fr.shikkanime.caches

import fr.shikkanime.entities.enums.CountryCode
import java.time.LocalDate
import java.util.*

data class CountryCodeLocalDateKeyCache(
val member: UUID?,
val countryCode: CountryCode,
val localDate: LocalDate
)
51 changes: 51 additions & 0 deletions src/main/kotlin/fr/shikkanime/controllers/api/AnimeController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,55 @@ class AnimeController : HasPageableRoute() {
)
)
}

@Path("/member-weekly")
@Get
@JWTAuthenticated
@OpenAPI(
"Get member weekly anime",
[
OpenAPIResponse(
200,
"Member weekly anime found",
Array<WeeklyAnimesDto>::class,
),
OpenAPIResponse(
400,
"Invalid week format",
MessageDto::class
),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun getMemberWeekly(
@JWTUser uuid: UUID,
@QueryParam("country", description = "By default: FR", type = CountryCode::class) countryParam: String?,
@QueryParam("date", description = "By default: today", type = String::class) dateParam: String?,
): Response {
val parsedDate = if (dateParam != null) {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")

try {
LocalDate.parse(dateParam, formatter)
} catch (e: Exception) {
return Response.badRequest(
MessageDto(
MessageDto.Type.ERROR,
"Invalid week format",
)
)
}
} else {
LocalDate.now()
}

return Response.ok(
animeCacheService.getWeeklyAnimes(
uuid,
parsedDate!!.minusDays(parsedDate.dayOfWeek.value.toLong() - 1),
CountryCode.fromNullable(countryParam) ?: CountryCode.FR
)
)
}
}
138 changes: 138 additions & 0 deletions src/main/kotlin/fr/shikkanime/controllers/api/MemberController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package fr.shikkanime.controllers.api

import com.google.inject.Inject
import fr.shikkanime.converters.AbstractConverter
import fr.shikkanime.dtos.AllFollowedEpisodeDto
import fr.shikkanime.dtos.GenericDto
import fr.shikkanime.dtos.MemberDto
import fr.shikkanime.services.MemberFollowAnimeService
import fr.shikkanime.services.MemberFollowEpisodeService
import fr.shikkanime.services.MemberService
import fr.shikkanime.utils.StringUtils
import fr.shikkanime.utils.routes.*
import fr.shikkanime.utils.routes.method.Delete
import fr.shikkanime.utils.routes.method.Post
import fr.shikkanime.utils.routes.method.Put
import fr.shikkanime.utils.routes.openapi.OpenAPI
import fr.shikkanime.utils.routes.openapi.OpenAPIResponse
import fr.shikkanime.utils.routes.param.BodyParam
import java.util.*

@Controller("/api/v1/members")
class MemberController {
@Inject
private lateinit var memberService: MemberService

@Inject
private lateinit var memberFollowAnimeService: MemberFollowAnimeService

@Inject
private lateinit var memberFollowEpisodeService: MemberFollowEpisodeService

@Path("/private-register")
@Post
@OpenAPI(
description = "Register a private member",
responses = [
OpenAPIResponse(201, "Private member registered", Map::class),
]
)
private fun registerPrivateMember(): Response {
var identifier: String

do {
identifier = StringUtils.generateRandomString(12)
} while (memberService.findPrivateMember(identifier) != null)

memberService.savePrivateMember(identifier)
return Response.created(mapOf("identifier" to identifier))
}

@Path("/private-login")
@Post
@OpenAPI(
description = "Login a private member",
responses = [
OpenAPIResponse(200, "Private member logged in"),
]
)
private fun loginPrivateMember(@BodyParam identifier: String): Response {
val privateMember = memberService.findPrivateMember(identifier) ?: return Response.notFound()
return Response.ok(AbstractConverter.convert(privateMember, MemberDto::class.java))
}

@Path("/animes")
@Put
@JWTAuthenticated
@OpenAPI(
description = "Follow an anime",
responses = [
OpenAPIResponse(200, "Anime followed successfully"),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun followAnime(@JWTUser uuidUser: UUID, @BodyParam anime: GenericDto): Response {
return memberFollowAnimeService.follow(uuidUser, anime)
}

@Path("/animes")
@Delete
@JWTAuthenticated
@OpenAPI(
description = "Unfollow an anime",
responses = [
OpenAPIResponse(200, "Anime unfollowed successfully"),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun unfollowAnime(@JWTUser uuidUser: UUID, @BodyParam anime: GenericDto): Response {
return memberFollowAnimeService.unfollow(uuidUser, anime)
}

@Path("/follow-all-episodes")
@Put
@JWTAuthenticated
@OpenAPI(
description = "Follow all episodes of an anime",
responses = [
OpenAPIResponse(200, "Episodes followed successfully", AllFollowedEpisodeDto::class),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun followAllEpisodes(@JWTUser uuidUser: UUID, @BodyParam anime: GenericDto): Response {
return memberFollowEpisodeService.followAll(uuidUser, anime)
}

@Path("/episodes")
@Put
@JWTAuthenticated
@OpenAPI(
description = "Follow an episode",
responses = [
OpenAPIResponse(200, "Episode followed successfully"),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun followEpisode(@JWTUser uuidUser: UUID, @BodyParam episode: GenericDto): Response {
return memberFollowEpisodeService.follow(uuidUser, episode)
}

@Path("/episodes")
@Delete
@JWTAuthenticated
@OpenAPI(
description = "Unfollow an episode",
responses = [
OpenAPIResponse(200, "Episode unfollowed successfully"),
OpenAPIResponse(401, "Unauthorized")
],
security = true
)
private fun unfollowEpisode(@JWTUser uuidUser: UUID, @BodyParam episode: GenericDto): Response {
return memberFollowEpisodeService.unfollow(uuidUser, episode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.shikkanime.converters.member

import com.google.inject.Inject
import fr.shikkanime.converters.AbstractConverter
import fr.shikkanime.dtos.MemberDto
import fr.shikkanime.dtos.TokenDto
import fr.shikkanime.entities.Member
import fr.shikkanime.services.MemberFollowAnimeService
import fr.shikkanime.services.MemberFollowEpisodeService
import fr.shikkanime.utils.withUTCString

class MemberToMemberDtoConverter : AbstractConverter<Member, MemberDto>() {
@Inject
private lateinit var memberFollowAnimeService: MemberFollowAnimeService

@Inject
private lateinit var memberFollowEpisodeService: MemberFollowEpisodeService

override fun convert(from: Member): MemberDto {
val tokenDto = convert(from, TokenDto::class.java)
val followedEpisodes = memberFollowEpisodeService.getAllFollowedEpisodes(from)

return MemberDto(
uuid = from.uuid!!,
token = tokenDto.token!!,
creationDateTime = from.creationDateTime.withUTCString(),
lastUpdateDateTime = from.lastUpdateDateTime.withUTCString(),
isPrivate = from.isPrivate,
followedAnimes = memberFollowAnimeService.getAllFollowedAnimes(from).mapNotNull { it.uuid },
followedEpisodes = followedEpisodes.mapNotNull { it.uuid },
totalDuration = followedEpisodes.sumOf { it.duration }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class MemberToTokenDtoConverter : AbstractConverter<Member, TokenDto>() {
.withAudience(Constant.jwtAudience)
.withIssuer(Constant.jwtDomain)
.withClaim("uuid", from.uuid.toString())
.withClaim("isPrivate", from.isPrivate)
.withClaim("username", from.username)
.withClaim("creationDateTime", from.creationDateTime.toString())
.withClaim("roles", from.roles.map { it.name })
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/fr/shikkanime/dtos/AllFollowedEpisodeDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.shikkanime.dtos

import java.util.*

data class AllFollowedEpisodeDto(
val data: Set<UUID>,
val duration: Long,
)
8 changes: 8 additions & 0 deletions src/main/kotlin/fr/shikkanime/dtos/GenericDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.shikkanime.dtos

import java.io.Serializable
import java.util.*

data class GenericDto(
val uuid: UUID,
) : Serializable
14 changes: 14 additions & 0 deletions src/main/kotlin/fr/shikkanime/dtos/MemberDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package fr.shikkanime.dtos

import java.util.*

data class MemberDto(
val uuid: UUID,
val token: String,
val creationDateTime: String,
val lastUpdateDateTime: String,
val isPrivate: Boolean,
val followedAnimes: List<UUID>,
val followedEpisodes: List<UUID>,
val totalDuration: Long,
)
6 changes: 5 additions & 1 deletion src/main/kotlin/fr/shikkanime/entities/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import java.util.*
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
class Member(
override val uuid: UUID? = null,
@Column(name = "creation_date_time")
@Column(nullable = false, name = "creation_date_time")
val creationDateTime: ZonedDateTime = ZonedDateTime.now(),
@Column(nullable = true, name = "last_update_date_time")
var lastUpdateDateTime: ZonedDateTime = ZonedDateTime.now(),
@Column(nullable = false, name = "is_private")
val isPrivate: Boolean = false,
@Column(nullable = false, unique = true)
val username: String? = null,
@Column(nullable = false, name = "encrypted_password")
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/fr/shikkanime/entities/MemberFollowAnime.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fr.shikkanime.entities

import jakarta.persistence.*
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy
import java.time.ZonedDateTime
import java.util.*

@Entity
@Table(name = "member_follow_anime")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
class MemberFollowAnime(
override val uuid: UUID? = null,
@Column(nullable = false, name = "follow_date_time")
val followDateTime: ZonedDateTime = ZonedDateTime.now(),
@ManyToOne(optional = false)
val member: Member? = null,
@ManyToOne(optional = false)
val anime: Anime? = null,
) : ShikkEntity(uuid)
21 changes: 21 additions & 0 deletions src/main/kotlin/fr/shikkanime/entities/MemberFollowEpisode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fr.shikkanime.entities

import jakarta.persistence.*
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy
import java.time.ZonedDateTime
import java.util.*

@Entity
@Table(name = "member_follow_episode")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
class MemberFollowEpisode(
override val uuid: UUID? = null,
@Column(nullable = false, name = "follow_date_time")
val followDateTime: ZonedDateTime = ZonedDateTime.now(),
@ManyToOne(optional = false)
val member: Member? = null,
@ManyToOne(optional = false)
val episode: EpisodeMapping? = null,
) : ShikkEntity(uuid)
14 changes: 13 additions & 1 deletion src/main/kotlin/fr/shikkanime/modules/HTTP.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package fr.shikkanime.modules

import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.LoggerFactory
import freemarker.cache.ClassTemplateLoader
import io.github.smiley4.ktorswaggerui.SwaggerUI
import io.github.smiley4.ktorswaggerui.data.AuthScheme
import io.github.smiley4.ktorswaggerui.data.AuthType
import io.ktor.http.*
import io.ktor.serialization.gson.*
import io.ktor.server.application.*
Expand All @@ -14,6 +17,9 @@ import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import java.util.logging.Level

private val logger = LoggerFactory.getLogger("HTTP")

fun Application.configureHTTP() {
install(Compression) {
Expand All @@ -34,6 +40,7 @@ fun Application.configureHTTP() {
}
install(StatusPages) {
exception<Throwable> { call, cause ->
logger.log(Level.SEVERE, "Internal server error", cause)
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
status(HttpStatusCode.NotFound) { call, _ ->
Expand All @@ -53,6 +60,11 @@ fun Application.configureHTTP() {
install(CachingHeaders) {
}
install(SwaggerUI) {
securityScheme("BearerAuth") {
type = AuthType.HTTP
scheme = AuthScheme.BEARER
bearerFormat = "jwt"
}
swagger {
swaggerUrl = "api/swagger"
forwardRoot = false
Expand All @@ -69,5 +81,5 @@ fun Application.configureHTTP() {
}

private fun isNotSiteRoute(path: String, errorCode: String): Boolean {
return path.contains(".") || path.startsWith("/$errorCode") || (path.startsWith("/api") && path.startsWith("/admin"))
return path.contains(".") || path.startsWith("/$errorCode") || path.startsWith("/api") || path.startsWith("/admin")
}
Loading

0 comments on commit a62b3ac

Please sign in to comment.