├── .github
└── CODEOWNERS
├── settings.gradle
├── gradle
├── monitor.gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── logging.gradle
├── slack.gradle
├── db.gradle
├── jetbrains.gradle
├── core.gradle
├── test.gradle
└── spring.gradle
├── src
├── main
│ ├── resources
│ │ ├── persona
│ │ │ └── text
│ │ │ │ ├── small
│ │ │ │ ├── 1.svg
│ │ │ │ ├── 4.svg
│ │ │ │ ├── 5.svg
│ │ │ │ ├── 7.svg
│ │ │ │ ├── 0.svg
│ │ │ │ ├── 9.svg
│ │ │ │ ├── 6.svg
│ │ │ │ ├── 2.svg
│ │ │ │ ├── 3.svg
│ │ │ │ └── 8.svg
│ │ │ │ ├── medium
│ │ │ │ ├── 1.svg
│ │ │ │ ├── 4.svg
│ │ │ │ ├── 7.svg
│ │ │ │ ├── 5.svg
│ │ │ │ ├── 0.svg
│ │ │ │ ├── 9.svg
│ │ │ │ ├── 6.svg
│ │ │ │ ├── 3.svg
│ │ │ │ ├── 2.svg
│ │ │ │ └── 8.svg
│ │ │ │ └── large
│ │ │ │ ├── _n.svg
│ │ │ │ ├── _u.svg
│ │ │ │ ├── _m.svg
│ │ │ │ ├── _r.svg
│ │ │ │ ├── _i.svg
│ │ │ │ ├── _p.svg
│ │ │ │ ├── _q.svg
│ │ │ │ ├── _v.svg
│ │ │ │ ├── _o.svg
│ │ │ │ ├── L.svg
│ │ │ │ ├── T.svg
│ │ │ │ ├── _e.svg
│ │ │ │ ├── _s.svg
│ │ │ │ ├── hyphens.svg
│ │ │ │ ├── _a.svg
│ │ │ │ ├── _c.svg
│ │ │ │ ├── _g.svg
│ │ │ │ ├── 1.svg
│ │ │ │ ├── _y.svg
│ │ │ │ ├── F.svg
│ │ │ │ ├── _w.svg
│ │ │ │ ├── _z.svg
│ │ │ │ ├── I.svg
│ │ │ │ ├── _l.svg
│ │ │ │ ├── E.svg
│ │ │ │ ├── H.svg
│ │ │ │ ├── U.svg
│ │ │ │ ├── _f.svg
│ │ │ │ ├── _h.svg
│ │ │ │ ├── J.svg
│ │ │ │ ├── _j.svg
│ │ │ │ ├── _x.svg
│ │ │ │ ├── P.svg
│ │ │ │ ├── _t.svg
│ │ │ │ ├── V.svg
│ │ │ │ ├── Y.svg
│ │ │ │ ├── 4.svg
│ │ │ │ ├── 7.svg
│ │ │ │ ├── G.svg
│ │ │ │ ├── O.svg
│ │ │ │ ├── _b.svg
│ │ │ │ ├── _d.svg
│ │ │ │ ├── N.svg
│ │ │ │ ├── 5.svg
│ │ │ │ ├── A.svg
│ │ │ │ ├── C.svg
│ │ │ │ ├── D.svg
│ │ │ │ ├── R.svg
│ │ │ │ ├── Z.svg
│ │ │ │ ├── 0.svg
│ │ │ │ ├── 9.svg
│ │ │ │ ├── K.svg
│ │ │ │ ├── M.svg
│ │ │ │ ├── Q.svg
│ │ │ │ ├── _k.svg
│ │ │ │ ├── W.svg
│ │ │ │ ├── B.svg
│ │ │ │ ├── 6.svg
│ │ │ │ ├── 2.svg
│ │ │ │ ├── 3.svg
│ │ │ │ ├── 8.svg
│ │ │ │ ├── S.svg
│ │ │ │ └── X.svg
│ │ ├── github-graphql
│ │ │ ├── contribution-year.graphql
│ │ │ ├── contribution-count-by-year.graphql
│ │ │ ├── contribution-count-by-year-and-week.graphql
│ │ │ └── stargazer.graphql
│ │ └── application.properties
│ └── kotlin
│ │ └── org
│ │ └── gitanimals
│ │ ├── render
│ │ ├── domain
│ │ │ ├── EntryPoint.kt
│ │ │ ├── event
│ │ │ │ ├── Visited.kt
│ │ │ │ ├── UsernameChanged.kt
│ │ │ │ ├── NewUserCreated.kt
│ │ │ │ ├── UserYesterdayReport.kt
│ │ │ │ ├── PersonaDeleted.kt
│ │ │ │ └── UserContributionUpdated.kt
│ │ │ ├── response
│ │ │ │ ├── NewPetDropRateDistribution.kt
│ │ │ │ └── PersonaResponse.kt
│ │ │ ├── IdempotencyRepository.kt
│ │ │ ├── value
│ │ │ │ ├── Level.kt
│ │ │ │ └── Contribution.kt
│ │ │ ├── request
│ │ │ │ └── PersonaChangeRequest.kt
│ │ │ ├── Idempotency.kt
│ │ │ ├── UserAuthInfo.kt
│ │ │ ├── extension
│ │ │ │ └── RenderFieldTypeExtension.kt
│ │ │ ├── UserStatisticRepository.kt
│ │ │ ├── PersonaStatisticRepository.kt
│ │ │ ├── UserStatisticService.kt
│ │ │ ├── AbstractTime.kt
│ │ │ ├── UserRepository.kt
│ │ │ ├── Field.kt
│ │ │ └── PersonaStatisticService.kt
│ │ ├── controller
│ │ │ ├── request
│ │ │ │ ├── ChangeFieldRequest.kt
│ │ │ │ ├── AddPersonaRequest.kt
│ │ │ │ ├── UsernameAndPersonaIdRequest.kt
│ │ │ │ └── AddMultiplyPersonaRequest.kt
│ │ │ ├── response
│ │ │ │ ├── PersonaEvolutionableResponse.kt
│ │ │ │ ├── TotalUserResponse.kt
│ │ │ │ ├── TotalPersonaResponse.kt
│ │ │ │ ├── PersonaEnumResponse.kt
│ │ │ │ ├── BackgroundResponse.kt
│ │ │ │ ├── UserResponse.kt
│ │ │ │ └── PersonaResponse.kt
│ │ │ ├── UserStatisticController.kt
│ │ │ ├── PersonaStatisticController.kt
│ │ │ ├── PersonaEvolutionController.kt
│ │ │ ├── InternalAnimationController.kt
│ │ │ └── AnimationController.kt
│ │ ├── saga
│ │ │ ├── event
│ │ │ │ ├── GavePoint.kt
│ │ │ │ └── CouponUsed.kt
│ │ │ └── UsedCouponSagaHandlers.kt
│ │ ├── app
│ │ │ ├── request
│ │ │ │ ├── MergePersonaRequest.kt
│ │ │ │ └── MergePersonaV2Request.kt
│ │ │ ├── ContributionApi.kt
│ │ │ ├── GithubRestApi.kt
│ │ │ ├── PersonaEvolutionFacade.kt
│ │ │ ├── IdentityApi.kt
│ │ │ └── UserStatisticSchedule.kt
│ │ └── infra
│ │ │ ├── PersonaDeletedEventHandler.kt
│ │ │ ├── event
│ │ │ └── NewPetDropRateDistributionEvent.kt
│ │ │ ├── CacheConfigurer.kt
│ │ │ └── CustomExecutorConfigurer.kt
│ │ ├── guild
│ │ ├── app
│ │ │ ├── response
│ │ │ │ ├── GuildIconsResponse.kt
│ │ │ │ ├── GuildBackgroundResponse.kt
│ │ │ │ ├── GuildsResponse.kt
│ │ │ │ └── GuildPagingResponse.kt
│ │ │ ├── LeaveGuildFacade.kt
│ │ │ ├── DenyJoinGuildFacade.kt
│ │ │ ├── GetJoinedGuildFacade.kt
│ │ │ ├── request
│ │ │ │ └── CreateGuildRequest.kt
│ │ │ ├── KickGuildFacade.kt
│ │ │ ├── AcceptJoinGuildFacade.kt
│ │ │ ├── ChangeGuildFacade.kt
│ │ │ ├── ChangeMainPersonaFacade.kt
│ │ │ ├── SearchGuildFacade.kt
│ │ │ ├── RenderApi.kt
│ │ │ └── IdentityApi.kt
│ │ ├── controller
│ │ │ └── request
│ │ │ │ └── JoinGuildRequest.kt
│ │ ├── domain
│ │ │ ├── RandomGuildCache.kt
│ │ │ ├── extension
│ │ │ │ └── GuildFieldTypeExtension.kt
│ │ │ ├── request
│ │ │ │ ├── ChangeGuildRequest.kt
│ │ │ │ └── CreateLeaderRequest.kt
│ │ │ ├── Leader.kt
│ │ │ ├── GuildIcons.kt
│ │ │ ├── event
│ │ │ │ └── GuildContributionUpdated.kt
│ │ │ ├── AbstractTime.kt
│ │ │ └── SearchFilter.kt
│ │ ├── saga
│ │ │ ├── event
│ │ │ │ └── PersonaDeleted.kt
│ │ │ └── PersonaDeletedSagaHandler.kt
│ │ └── infra
│ │ │ ├── event
│ │ │ └── UserContributionUpdated.kt
│ │ │ ├── HttpClientErrorHandler.kt
│ │ │ ├── GuildRedisEventSubscriber.kt
│ │ │ └── GuildUpdateGuildContributionMessageListener.kt
│ │ ├── core
│ │ ├── AggregateRoot.kt
│ │ ├── PersonaGrade.kt
│ │ ├── lock
│ │ │ ├── LockAcquireFailException.kt
│ │ │ ├── DistributedLock.kt
│ │ │ └── RedisDistributedLockService.kt
│ │ ├── Mode.kt
│ │ ├── IdGenerator.kt
│ │ ├── ErrorResponse.kt
│ │ ├── Exception.kt
│ │ ├── auth
│ │ │ ├── RequiredUserEntryPoints.kt
│ │ │ ├── RequiredUserEntryPointAspect.kt
│ │ │ ├── UserEntryPointValidationExtension.kt
│ │ │ └── InternalAuthRequestInterceptor.kt
│ │ ├── redis
│ │ │ ├── AsyncRedisPubSubEvent.kt
│ │ │ ├── RedisPubSubChannel.kt
│ │ │ ├── TransactionCommitRedisPubSubEvent.kt
│ │ │ ├── RedisPubSubEventListener.kt
│ │ │ ├── RedisConfiguration.kt
│ │ │ ├── TransactionCommitRedisPubSubEventListener.kt
│ │ │ └── TraceableMessageListener.kt
│ │ ├── UpdateUserOrchestrator.kt
│ │ ├── clock.kt
│ │ ├── PersonaEvolutionType.kt
│ │ ├── extension
│ │ │ ├── HttpResponseExtension.kt
│ │ │ └── StringExtension.kt
│ │ ├── interceptor
│ │ │ ├── InternalApiInterceptor.kt
│ │ │ └── InterceptorConfigurer.kt
│ │ ├── ratelimit
│ │ │ └── RateLimitable.kt
│ │ ├── slack
│ │ │ └── SlackSender.kt
│ │ ├── HttpClientErrorHandler.kt
│ │ ├── DomainEventPublisher.kt
│ │ ├── filter
│ │ │ └── CorsFilter.kt
│ │ ├── JacksonConfig.kt
│ │ ├── appender
│ │ │ └── SlackAppender.kt
│ │ └── GracefulShutdownDispatcher.kt
│ │ ├── rank
│ │ ├── controller
│ │ │ └── response
│ │ │ │ ├── RankTotalCountResponse.kt
│ │ │ │ ├── RankResponses.kt
│ │ │ │ └── RankHistoryResponse.kt
│ │ ├── app
│ │ │ ├── RankContributionApi.kt
│ │ │ ├── RenderApi.kt
│ │ │ ├── IdentityApi.kt
│ │ │ ├── GuildApi.kt
│ │ │ └── GetRankByUsernameFacade.kt
│ │ ├── domain
│ │ │ ├── response
│ │ │ │ └── RankResponse.kt
│ │ │ ├── event
│ │ │ │ └── RankUpdated.kt
│ │ │ ├── history
│ │ │ │ ├── Winner.kt
│ │ │ │ ├── request
│ │ │ │ │ └── InitRankHistoryRequest.kt
│ │ │ │ ├── RankHistoryRepository.kt
│ │ │ │ ├── RankHistory.kt
│ │ │ │ └── RankHistoryService.kt
│ │ │ ├── Rank.kt
│ │ │ ├── GuildContributionRankRepository.kt
│ │ │ ├── UserContributionRankRepository.kt
│ │ │ ├── RankQueryRepository.kt
│ │ │ ├── AbstractTime.kt
│ │ │ ├── UserContributionRank.kt
│ │ │ └── GuildContributionRank.kt
│ │ └── infra
│ │ │ ├── event
│ │ │ ├── GuildContributionUpdated.kt
│ │ │ └── UserContributionUpdated.kt
│ │ │ ├── RankCacheConfigurer.kt
│ │ │ ├── HttpClientErrorHandler.kt
│ │ │ └── listener
│ │ │ ├── RankRedisEventSubscriber.kt
│ │ │ ├── UpdateUserContributionMessageListener.kt
│ │ │ └── RankUpdateGuildContributionMessageListener.kt
│ │ ├── star
│ │ ├── domain
│ │ │ ├── StargazersRepository.kt
│ │ │ ├── Stargazer.kt
│ │ │ └── StargazerService.kt
│ │ ├── controller
│ │ │ └── StargazerController.kt
│ │ └── infra
│ │ │ └── StargazerBatchJob.kt
│ │ ├── supports
│ │ ├── event
│ │ │ ├── SlackInteracted.kt
│ │ │ └── SlackReplied.kt
│ │ ├── deadletter
│ │ │ ├── DeadLetterEvent.kt
│ │ │ ├── DeadLetterListenConfigurer.kt
│ │ │ ├── DeadLetterRedisMessageListenerConfiguration.kt
│ │ │ └── DeadLetterEventPublisher.kt
│ │ ├── orchestrate
│ │ │ └── IdentityApi.kt
│ │ └── schedule
│ │ │ └── SchedulerTraceIdAspect.kt
│ │ └── Application.kt
└── test
│ ├── resources
│ ├── persona
│ │ └── goose
│ │ │ └── test.html
│ └── application.properties
│ └── kotlin
│ └── org
│ └── gitanimals
│ ├── play
│ └── PlayGround.kt
│ ├── render
│ ├── supports
│ │ ├── DomainEventHolder.kt
│ │ ├── RedisContainer.kt
│ │ ├── IntegrationTest.kt
│ │ └── SagaCapture.kt
│ ├── domain
│ │ ├── UserFixture.kt
│ │ └── UserStatisticsServiceTest.kt
│ └── app
│ │ └── UserStatisticScheduleTest.kt
│ ├── guild
│ ├── supports
│ │ ├── RedisContainer.kt
│ │ ├── GuildSagaCapture.kt
│ │ └── MockApiConfiguration.kt
│ └── domain
│ │ └── Fixture.kt
│ └── core
│ ├── extension
│ └── StringExtensionTest.kt
│ ├── lock
│ └── DistributedLockTest.kt
│ └── CalculatePersonaDropRate.kt
├── deploy
├── filebeat
│ ├── filebeat.yml
│ └── Dockerfile
└── api
│ └── Dockerfile
├── .idea
└── checkstyle-idea.xml
├── LICENSE.md
├── .gitignore
└── gradle.properties
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @devxb
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 |
2 | rootProject.name = 'gitanimals'
3 |
4 |
--------------------------------------------------------------------------------
/gradle/monitor.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-goods/gitanimals/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle/logging.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation "co.elastic.logging:logback-ecs-encoder:${logbackEcsEncoderVersion}"
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/EntryPoint.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | enum class EntryPoint {
4 | GITHUB,
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/4.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/test/resources/persona/goose/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/5.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/7.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_n.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_u.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/0.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/9.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/4.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/7.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_m.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/response/GuildIconsResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app.response
2 |
3 | data class GuildIconsResponse(
4 | val icons: List,
5 | )
6 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/5.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/6.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/controller/request/JoinGuildRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.controller.request
2 |
3 | data class JoinGuildRequest(
4 | val personaId: String,
5 | )
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/request/ChangeFieldRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.request
2 |
3 | data class ChangeFieldRequest(
4 | val type: String,
5 | )
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/event/Visited.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.event
2 |
3 | data class Visited(
4 | val username: String,
5 | val traceId: String,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/AggregateRoot.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | @Target(AnnotationTarget.CLASS)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class AggregateRoot
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/PersonaGrade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | enum class PersonaGrade {
4 |
5 | DEFAULT,
6 | EVOLUTION,
7 | COLLABORATOR,
8 | ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/controller/response/RankTotalCountResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.controller.response
2 |
3 | data class RankTotalCountResponse(
4 | val count: Int,
5 | )
6 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_r.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_i.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_p.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_q.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_v.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/lock/LockAcquireFailException.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.lock
2 |
3 | class LockAcquireFailException(
4 | override val message: String,
5 | ) : RuntimeException(message)
6 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/0.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_o.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/9.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/Mode.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | enum class Mode {
4 | FARM,
5 | LINE,
6 | LINE_NO_CONTRIBUTION,
7 | NAME_WITH_LEVEL,
8 | NONE,
9 | ;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/event/UsernameChanged.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.event
2 |
3 | data class UsernameChanged(
4 | val previousName: String,
5 | val changedName: String,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/L.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/T.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_e.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_s.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/hyphens.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/small/8.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/saga/event/GavePoint.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.saga.event
2 |
3 | data class GavePoint(
4 | val username: String,
5 | val point: Long,
6 | val contributions: Int,
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_a.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_c.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_g.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/6.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle/slack.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation "com.slack.api:bolt:${slackVersion}"
3 | implementation "com.slack.api:bolt-servlet:${slackVersion}"
4 | implementation "com.slack.api:bolt-jetty:${slackVersion}"
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/response/PersonaEvolutionableResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.response
2 |
3 | data class PersonaEvolutionableResponse(
4 | val evolutionAble: Boolean
5 | )
6 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/1.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_y.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/request/MergePersonaRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app.request
2 |
3 | data class MergePersonaRequest(
4 | val increasePersonaId: String,
5 | val deletePersonaId: String,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/response/NewPetDropRateDistribution.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.response
2 |
3 | data class NewPetDropRateDistribution(
4 | val dropRate: Double,
5 | val count: Int,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/F.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_w.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_z.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/request/AddPersonaRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.request
2 |
3 | data class AddPersonaRequest(
4 | val id: Long,
5 | val name: String,
6 | val level: Int,
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/I.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_l.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/request/MergePersonaV2Request.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app.request
2 |
3 | data class MergePersonaV2Request(
4 | val increasePersonaId: String,
5 | val deletePersonaId: List,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/star/domain/StargazersRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.star.domain
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository
4 |
5 | interface StargazersRepository : JpaRepository
6 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/E.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/gradle/db.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation "mysql:mysql-connector-java:${mysqlConnectorVersion}"
3 | implementation "com.github.f4b6a3:tsid-creator:${snowflakeVersion}"
4 |
5 | testRuntimeOnly "com.h2database:h2:${h2Version}"
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/request/UsernameAndPersonaIdRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.request
2 |
3 | data class UsernameAndPersonaIdRequest(
4 | val username: String,
5 | val personaId: Long,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/IdempotencyRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository
4 |
5 | interface IdempotencyRepository : JpaRepository
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/event/NewUserCreated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.event
2 |
3 | data class NewUserCreated(
4 | val userId: Long,
5 | val username: String,
6 | val newUserCreated: Boolean = true,
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/saga/event/CouponUsed.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.saga.event
2 |
3 | data class CouponUsed(
4 | val userId: Long,
5 | val username: String,
6 | val code: String,
7 | val dynamic: String,
8 | )
9 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/H.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/U.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_f.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_h.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/response/GuildBackgroundResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app.response
2 |
3 | import org.gitanimals.core.FieldType
4 |
5 | data class GuildBackgroundResponse(
6 | val backgrounds: List
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/request/AddMultiplyPersonaRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.request
2 |
3 | data class AddMultiplyPersonaRequest(
4 | val idempotencyKey: String,
5 | val personaName: String,
6 | )
7 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/J.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_j.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/controller/response/RankResponses.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.controller.response
2 |
3 | import org.gitanimals.rank.domain.response.RankResponse
4 |
5 | data class RankResponses(
6 | val ranks: List,
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/P.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_t.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/medium/8.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/V.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/Y.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/4.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/7.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/G.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/O.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_b.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_d.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/app/RankContributionApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.app
2 |
3 | import java.time.LocalDate
4 |
5 | fun interface RankContributionApi {
6 |
7 | fun getContributionsBySpecificDays(username: String, from: LocalDate, to: LocalDate): Int
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/N.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/5.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/A.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/C.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/response/RankResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.response
2 |
3 | data class RankResponse(
4 | val id: String,
5 | val rank: Int,
6 | val image: String,
7 | val name: String,
8 | val contributions: Long,
9 | )
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/ContributionApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app
2 |
3 | interface ContributionApi {
4 |
5 | fun getContributionCount(username: String, years: List): Map
6 |
7 | fun getAllContributionYears(username: String): List
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/D.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/R.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/Z.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/0.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/9.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/K.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/M.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/Q.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/_k.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/IdGenerator.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import com.github.f4b6a3.tsid.TsidFactory
4 |
5 | object IdGenerator {
6 |
7 | private val tsidFactory = TsidFactory.newInstance256()
8 |
9 | fun generate(): Long = tsidFactory.create().toLong()
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/W.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/B.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/value/Level.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.value
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Embeddable
5 |
6 | @Embeddable
7 | data class Level(
8 | @Column(name = "level", nullable = false)
9 | var value: Long,
10 | )
11 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/6.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/2.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/3.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/star/domain/Stargazer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.star.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Entity
5 | import jakarta.persistence.Id
6 |
7 | @Entity
8 | class Stargazer(
9 | @Id
10 | @Column(name = "login")
11 | val login: String,
12 | )
13 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/RandomGuildCache.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain
2 |
3 | import org.springframework.data.domain.Page
4 |
5 | interface RandomGuildCache {
6 |
7 | fun get(key: Int, text: String, pageNumber: Int, filter: SearchFilter): Page
8 |
9 | fun updateForce()
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/resources/github-graphql/contribution-year.graphql:
--------------------------------------------------------------------------------
1 | query {
2 | rateLimit {
3 | limit
4 | cost
5 | remaining
6 | resetAt
7 | used
8 | }
9 | user(login: "*{name}") {
10 | contributionsCollection {
11 | contributionYears
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/ErrorResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | data class ErrorResponse(
4 | val message: String,
5 | ) {
6 |
7 | companion object {
8 | fun from(exception: Exception): ErrorResponse =
9 | ErrorResponse(exception.message ?: exception.localizedMessage)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/event/UserYesterdayReport.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.event
2 |
3 | import java.time.ZonedDateTime
4 |
5 | data class UserYesterdayReport(
6 | val date: ZonedDateTime,
7 | val yesterdayNewUserCount: Int,
8 | val totalUserCount: Long,
9 | val serverName: String,
10 | )
11 |
--------------------------------------------------------------------------------
/deploy/filebeat/filebeat.yml:
--------------------------------------------------------------------------------
1 | filebeat.inputs:
2 | - type: log
3 | enabled: true
4 | paths:
5 | - /logs/*.json
6 | json:
7 | keys_under_root: true
8 | add_error_key: true
9 |
10 | output.elasticsearch:
11 | hosts: ["192.168.0.31:9200"]
12 | username: "${FILEBEAT_USERNAME}"
13 | password: "${FILEBEAT_PASSWORD}"
14 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/8.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/event/RankUpdated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.event
2 |
3 | import org.gitanimals.rank.domain.RankId
4 | import org.gitanimals.rank.domain.RankQueryRepository
5 |
6 | data class RankUpdated(
7 | val type: RankQueryRepository.RankType,
8 | val rankId: RankId,
9 | val score: Long,
10 | )
11 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/S.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/persona/text/large/X.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/Exception.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties
4 |
5 | @JsonIgnoreProperties(ignoreUnknown = true)
6 | class AuthorizationException(message: String) : IllegalArgumentException(message)
7 |
8 | val AUTHORIZATION_EXCEPTION = AuthorizationException("Authorization fail")
9 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/history/Winner.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.history
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Embeddable
5 |
6 | @Embeddable
7 | class Winner(
8 | @Column(name = "winner_id")
9 | val id: Long,
10 | @Column(name = "winner_name")
11 | val name: String,
12 | )
13 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/event/SlackInteracted.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.event
2 |
3 | data class SlackInteracted(
4 | val userId: String,
5 | val username: String,
6 | val slackChannel: String,
7 | val threadTs: String,
8 | val sourceKey: String,
9 | val payload: String,
10 | val traceId: String,
11 | )
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/saga/event/PersonaDeleted.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.saga.event
2 |
3 | import org.gitanimals.core.clock
4 | import java.time.Instant
5 |
6 | data class PersonaDeleted(
7 | val userId: Long,
8 | val username: String,
9 | val personaId: Long,
10 | val personaDeletedAt: Instant = clock.instant(),
11 | )
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/event/PersonaDeleted.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.event
2 |
3 | import org.gitanimals.core.clock
4 | import java.time.Instant
5 |
6 | data class PersonaDeleted(
7 | val userId: Long,
8 | val username: String,
9 | val personaId: Long,
10 | val personaDeletedAt: Instant = clock.instant(),
11 | )
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/response/TotalUserResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.response
2 |
3 | data class TotalUserResponse(
4 | val userCount: String,
5 | ) {
6 |
7 | companion object {
8 | fun from(totalUserCount: Long): TotalUserResponse =
9 | TotalUserResponse(totalUserCount.toString())
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/request/PersonaChangeRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.request
2 |
3 | data class PersonaChangeRequest(
4 | val personaId: String,
5 | val visible: Boolean,
6 | val type: VisibleChangeType = VisibleChangeType.DEFAULT
7 | )
8 |
9 | enum class VisibleChangeType {
10 | DEFAULT,
11 | APP,
12 | ;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/auth/RequiredUserEntryPoints.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.auth
2 |
3 | @Target(AnnotationTarget.FUNCTION)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class RequiredUserEntryPoints(
6 | val expected: Array,
7 | )
8 |
9 | enum class UserEntryPoint {
10 | ANY,
11 | GITHUB,
12 | APPLE,
13 | ;
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/infra/event/GuildContributionUpdated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.infra.event
2 |
3 | data class GuildContributionUpdated(
4 | val traceId: String,
5 | val guildId: Long,
6 | val guildTitle: String,
7 | val guildImage: String,
8 | val contributions: Long,
9 | val updatedContributions: Long,
10 | val timestamp: Long,
11 | )
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/history/request/InitRankHistoryRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.history.request
2 |
3 | import org.gitanimals.rank.domain.RankQueryRepository.RankType
4 |
5 | data class InitRankHistoryRequest(
6 | val rank: Int,
7 | val prize: Int,
8 | val rankType: RankType,
9 | val winnerId: Long,
10 | val winnerName: String,
11 | )
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/Rank.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | import jakarta.persistence.*
4 |
5 | @MappedSuperclass
6 | @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
7 | abstract class Rank(
8 | @Id
9 | @Column(name = "id")
10 | val id: Long,
11 |
12 | @Column(name = "image", nullable = false)
13 | val image: String,
14 | ) : AbstractTime()
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/response/TotalPersonaResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.response
2 |
3 | data class TotalPersonaResponse(
4 | private val personaCount: String,
5 | ) {
6 |
7 | companion object {
8 | fun from(totalPersonaCount: Long): TotalPersonaResponse =
9 | TotalPersonaResponse(totalPersonaCount.toString())
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/Idempotency.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Entity
5 | import jakarta.persistence.Id
6 | import jakarta.persistence.Table
7 |
8 | @Entity(name="idempotency")
9 | @Table(name="idempotency")
10 | class Idempotency(
11 | @Id
12 | @Column(name="id")
13 | val id: String
14 | )
15 |
--------------------------------------------------------------------------------
/gradle/jetbrains.gradle:
--------------------------------------------------------------------------------
1 | compileKotlin {
2 | kotlinOptions.jvmTarget = "${jvmTarget}"
3 | }
4 |
5 | compileTestKotlin {
6 | kotlinOptions.jvmTarget = "${jvmTarget}"
7 | }
8 |
9 | dependencies {
10 | implementation "org.jetbrains.kotlin:kotlin-reflect"
11 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactor"
12 |
13 | testImplementation "org.jetbrains.kotlin:kotlin-test"
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/history/RankHistoryRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.history
2 |
3 | import org.gitanimals.rank.domain.RankQueryRepository.RankType
4 | import org.springframework.data.jpa.repository.JpaRepository
5 |
6 | interface RankHistoryRepository: JpaRepository {
7 |
8 | fun findTop3ByRankTypeOrderByCreatedAtDesc(rankType: RankType): List
9 | }
10 |
--------------------------------------------------------------------------------
/gradle/core.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation "org.rooftopmsa:netx:${netxVersion}"
3 | implementation "com.github.ben-manes.caffeine:caffeine:${caffeineCacheVersion}"
4 | implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}"
5 | implementation "org.redisson:redisson-spring-boot-starter:${redissonVersion}"
6 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:${slf4jKotlinxVersion}"
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/response/GuildsResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app.response
2 |
3 | import org.gitanimals.guild.domain.Guild
4 |
5 | data class GuildsResponse(
6 | val guilds: List,
7 | ) {
8 |
9 | companion object {
10 |
11 | fun from(guilds: List): GuildsResponse {
12 | return GuildsResponse(guilds.map { GuildResponse.from(it) })
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/infra/event/UserContributionUpdated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.infra.event
2 |
3 | import java.time.Instant
4 |
5 | data class UserContributionUpdated(
6 | val traceId: String,
7 | val username: String,
8 | val contributions: Long,
9 | val updatedContributions: Long,
10 | val userContributionUpdated: Boolean,
11 | val contributionUpdatedAt: Instant,
12 | val timestamp: Long,
13 | )
14 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/redis/AsyncRedisPubSubEvent.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.redis
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore
4 | import org.gitanimals.core.filter.MDCFilter
5 | import org.slf4j.MDC
6 |
7 | abstract class AsyncRedisPubSubEvent(
8 | val apiUserId: String? = runCatching { MDC.get(MDCFilter.USER_ID) }.getOrNull(),
9 | val traceId: String,
10 | @JsonIgnore
11 | val channel: String,
12 | )
13 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/infra/event/UserContributionUpdated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.infra.event
2 |
3 | import java.time.Instant
4 |
5 | data class UserContributionUpdated(
6 | val traceId: String,
7 | val username: String,
8 | val contributions: Long,
9 | val updatedContributions: Long,
10 | val userContributionUpdated: Boolean,
11 | val contributionUpdatedAt: Instant,
12 | val timestamp: Long,
13 | )
14 |
--------------------------------------------------------------------------------
/src/main/resources/github-graphql/contribution-count-by-year.graphql:
--------------------------------------------------------------------------------
1 | query {
2 | rateLimit {
3 | limit
4 | cost
5 | remaining
6 | resetAt
7 | used
8 | }
9 | user(login: "*{name}") {
10 | contributionsCollection(from: "*{year}-01-01T00:00:00Z", to: "*{year}-12-31T23:59:59Z") {
11 | contributionCalendar {
12 | totalContributions
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/deploy/filebeat/Dockerfile:
--------------------------------------------------------------------------------
1 | # Filebeat Base Image
2 | FROM docker.elastic.co/beats/filebeat:8.10.0
3 |
4 | # Copy custom Filebeat configuration
5 | COPY filebeat.yml /usr/share/filebeat/filebeat.yml
6 |
7 | # Set permissions
8 | USER root
9 | RUN chmod go-w /usr/share/filebeat/filebeat.yml
10 |
11 | # Set working directory
12 | WORKDIR /usr/share/filebeat
13 |
14 | # Entry point
15 | ENTRYPOINT ["filebeat"]
16 | CMD ["-e", "-c", "/usr/share/filebeat/filebeat.yml"]
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/extension/GuildFieldTypeExtension.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain.extension
2 |
3 | import org.gitanimals.core.FieldType
4 |
5 | object GuildFieldTypeExtension {
6 |
7 | fun FieldType.isGuildField(): Boolean {
8 | return this in guildFields
9 | }
10 |
11 | private val guildFields = setOf(
12 | FieldType.FOLDER,
13 | FieldType.RED_COMPUTER,
14 | FieldType.RED_SOFA,
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/resources/github-graphql/contribution-count-by-year-and-week.graphql:
--------------------------------------------------------------------------------
1 | query {
2 | rateLimit {
3 | limit
4 | cost
5 | remaining
6 | resetAt
7 | used
8 | }
9 | user(login: "*{name}") {
10 | contributionsCollection(from: "*{yyyy-mm-dd-start}T00:00:00Z", to: "*{yyyy-mm-dd-end}T23:59:59Z") {
11 | contributionCalendar {
12 | totalContributions
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/UpdateUserOrchestrator.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import org.gitanimals.core.auth.UserEntryPoint
4 |
5 | fun interface UpdateUserOrchestrator {
6 |
7 | fun updateUsername(request: UpdateUserNameRequest)
8 |
9 | data class UpdateUserNameRequest(
10 | val id: Long,
11 | val authenticationId: String,
12 | val entryPoint: UserEntryPoint,
13 | val previousName: String,
14 | val changeName: String,
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/gradle/test.gradle:
--------------------------------------------------------------------------------
1 | test {
2 | useJUnitPlatform()
3 | }
4 |
5 | dependencies {
6 | testImplementation "io.kotest:kotest-runner-junit5:${kotestVersion}"
7 | testImplementation "io.kotest:kotest-assertions-core:${kotestVersion}"
8 | testImplementation "io.kotest.extensions:kotest-extensions-spring:${kotestExtensionSpringVersion}"
9 |
10 | testImplementation "org.testcontainers:testcontainers:${testContainerVersion}"
11 |
12 | testImplementation "io.rest-assured:rest-assured:${restAssuredVersion}"
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/value/Contribution.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.value
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Embeddable
5 | import java.time.Instant
6 |
7 | @Embeddable
8 | data class Contribution(
9 | @Column(name = "year", nullable = false)
10 | val year: Int,
11 | @Column(name = "contribution", nullable = false)
12 | var contribution: Int,
13 | @Column(name = "last_updated_contributions",)
14 | var lastUpdatedContribution: Instant,
15 | )
16 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/clock.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import java.time.Clock
4 | import java.time.Instant
5 | import java.time.ZoneId
6 | import java.time.ZonedDateTime
7 |
8 | var clock: Clock = Clock.systemUTC()
9 |
10 | fun instant() = Instant.now(clock)
11 |
12 | fun Instant.toZonedDateTime() = ZonedDateTime.ofInstant(this, clock.zone)
13 |
14 | fun Instant.toZonedDateTime(zoneId: ZoneId) = ZonedDateTime.ofInstant(this, zoneId)
15 |
16 | fun Instant.toKr() = ZonedDateTime.ofInstant(this, ZoneId.of("Asia/Seoul"))
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/UserAuthInfo.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Embeddable
5 | import jakarta.persistence.EnumType
6 | import jakarta.persistence.Enumerated
7 |
8 | @Embeddable
9 | class UserAuthInfo(
10 |
11 | @Enumerated(EnumType.STRING)
12 | @Column(name = "entry_point", nullable = true)
13 | val entryPoint: EntryPoint,
14 |
15 | @Column(name = "authentication_id", nullable = true)
16 | val authenticationId: String,
17 | )
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/redis/RedisPubSubChannel.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.redis
2 |
3 | object RedisPubSubChannel {
4 |
5 | const val USER_CONTRIBUTION_UPDATED = "USER_CONTRIBUTION_UPDATED"
6 | const val GUILD_CONTRIBUTION_UPDATED = "GUILD_CONTRIBUTION_UPDATED"
7 |
8 | const val SLACK_INTERACTED = "SLACK_INTERACTED"
9 | const val SLACK_REPLIED = "SLACK_REPLIED"
10 |
11 | const val DEAD_LETTER_OCCURRED = "DEAD_LETTER_OCCURRED"
12 |
13 | const val NEW_PET_DROP_RATE_DISTRIBUTION = "NEW_PET_DROP_RATE_DISTRIBUTION"
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/LeaveGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.auth.InternalAuth
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.springframework.stereotype.Component
6 |
7 | @Component
8 | class LeaveGuildFacade(
9 | private val internalAuth: InternalAuth,
10 | private val guildService: GuildService,
11 | ) {
12 |
13 | fun leave(guildId: Long) {
14 | val userId = internalAuth.getUserId()
15 |
16 | guildService.leave(guildId, userId)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/PersonaEvolutionType.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 |
4 | data class PersonaEvolution(
5 | val weight: Double,
6 | val type: PersonaEvolutionType,
7 | ) {
8 |
9 | companion object {
10 |
11 | val nothing = PersonaEvolution(
12 | weight = 0.0,
13 | type = PersonaEvolutionType.NOTHING,
14 | )
15 | }
16 | }
17 |
18 | enum class PersonaEvolutionType {
19 |
20 | NOTHING,
21 | LITTLE_CHICK,
22 | FLAMINGO,
23 | SLIME,
24 | CAT,
25 | GOOSE,
26 | PIG,
27 | ;
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/extension/RenderFieldTypeExtension.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.extension
2 |
3 | import org.gitanimals.core.FieldType
4 |
5 | object RenderFieldTypeExtension {
6 |
7 | fun FieldType.isRenderField(): Boolean {
8 | return this in renderFields
9 | }
10 |
11 | private val renderFields = FieldType.entries.asSequence()
12 | .filter { it != FieldType.LOGO_SHOWING }
13 | .filter { it != FieldType.FOLDER }
14 | .filter { it != FieldType.RED_SOFA }
15 | .filter { it != FieldType.RED_COMPUTER }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/request/ChangeGuildRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain.request
2 |
3 | import org.gitanimals.core.FieldType
4 | import org.gitanimals.core.largetTextAcceptableChars
5 |
6 | data class ChangeGuildRequest(
7 | val title: String,
8 | val body: String,
9 | val farmType: FieldType,
10 | val guildIcon: String,
11 | val autoJoin: Boolean,
12 | ) {
13 |
14 | fun requireValidTitle() {
15 | title.forEach {
16 | require(it in largetTextAcceptableChars) { "Cannot accept title \"$it\"" }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/extension/HttpResponseExtension.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.extension
2 |
3 | import jakarta.servlet.http.HttpServletResponse
4 | import org.springframework.http.HttpHeaders
5 |
6 | object HttpResponseExtension {
7 |
8 | fun HttpServletResponse.cacheControl(maxAgeSeconds: Int): HttpServletResponse {
9 | this.setHeader(
10 | HttpHeaders.CACHE_CONTROL,
11 | "no-cache, no-store, must-revalidate, max-age=$maxAgeSeconds"
12 | )
13 | this.setHeader(HttpHeaders.PRAGMA, "no-cache")
14 | return this
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/GuildContributionRankRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository
4 | import org.springframework.data.jpa.repository.Modifying
5 | import org.springframework.data.jpa.repository.Query
6 |
7 | interface GuildContributionRankRepository : JpaRepository {
8 |
9 | fun findByGuildId(guildId: Long): GuildContributionRank?
10 |
11 | @Modifying
12 | @Query("update GuildContributionRank as g set g.weeklyContributions = 0")
13 | fun initialWeeklyRanks()
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/GithubRestApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.service.annotation.GetExchange
6 |
7 | fun interface GithubRestApi {
8 |
9 | @GetExchange("/users/{username}")
10 | fun getGithubUser(@PathVariable("username") username: String): GithubUserResponse
11 |
12 | data class GithubUserResponse(
13 | @JsonProperty("login")
14 | val name: String,
15 | val id: String,
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/DenyJoinGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.auth.InternalAuth
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.springframework.stereotype.Service
6 |
7 | @Service
8 | class DenyJoinGuildFacade(
9 | private val internalAuth: InternalAuth,
10 | private val guildService: GuildService,
11 | ) {
12 |
13 | fun denyJoin(guildId: Long, denyUserId: Long) {
14 | val userId = internalAuth.getUserId()
15 |
16 | guildService.denyJoin(denierId = userId, guildId = guildId, denyUserId = denyUserId)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.auth.InternalAuth
4 | import org.gitanimals.guild.domain.Guild
5 | import org.gitanimals.guild.domain.GuildService
6 | import org.springframework.stereotype.Service
7 |
8 | @Service
9 | class GetJoinedGuildFacade(
10 | private val internalAuth: InternalAuth,
11 | private val guildService: GuildService,
12 | ) {
13 |
14 | fun getJoinedGuilds(): List {
15 | val userId = internalAuth.getUserId()
16 |
17 | return guildService.findAllGuildByUserId(userId)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app.request
2 |
3 | import org.gitanimals.core.FieldType
4 | import org.gitanimals.core.largetTextAcceptableChars
5 |
6 | data class CreateGuildRequest(
7 | val title: String,
8 | val body: String,
9 | val guildIcon: String,
10 | val autoJoin: Boolean,
11 | val farmType: FieldType,
12 | val personaId: String,
13 | ) {
14 |
15 | fun requireValidTitle() {
16 | title.forEach {
17 | require(it in largetTextAcceptableChars) { "Cannot accept title \"$it\"" }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/UserStatisticRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository
4 | import org.springframework.data.jpa.repository.Query
5 | import org.springframework.data.repository.query.Param
6 | import java.time.Instant
7 |
8 | interface UserStatisticRepository : JpaRepository {
9 |
10 | @Query("select count(u.id) from user u where u.createdAt between :startDay and :endDay")
11 | fun getDailyUserCount(
12 | @Param("startDay") startDay: Instant,
13 | @Param("endDay") endDay: Instant,
14 | ): Int
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/resources/github-graphql/stargazer.graphql:
--------------------------------------------------------------------------------
1 | query {
2 | rateLimit {
3 | limit
4 | cost
5 | remaining
6 | resetAt
7 | used
8 | }
9 | repository(owner: "git-goods", name: "gitanimals") {
10 | stargazers(first: 100, after: "*{endCursor}") {
11 | edges {
12 | starredAt
13 | node {
14 | login
15 | name
16 | url
17 | }
18 | }
19 | pageInfo {
20 | endCursor
21 | hasNextPage
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/history/RankHistory.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.history
2 |
3 | import jakarta.persistence.*
4 | import org.gitanimals.rank.domain.AbstractTime
5 | import org.gitanimals.rank.domain.RankQueryRepository.RankType
6 |
7 | @Entity
8 | @Table(name = "rank_history")
9 | class RankHistory(
10 | @Id
11 | val id: Long,
12 | @Column(name = "ranks")
13 | val ranks: Int,
14 | @Column(name = "prize")
15 | val prize: Int,
16 | @Enumerated(EnumType.STRING)
17 | @Column(name = "rank_type")
18 | val rankType: RankType,
19 | @Embedded
20 | val winner: Winner,
21 | ) : AbstractTime()
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/event/SlackReplied.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.event
2 |
3 | import org.gitanimals.core.IdGenerator
4 | import org.gitanimals.core.filter.MDCFilter.Companion.TRACE_ID
5 | import org.gitanimals.core.redis.AsyncRedisPubSubEvent
6 | import org.gitanimals.core.redis.RedisPubSubChannel
7 | import org.slf4j.MDC
8 |
9 | data class SlackReplied(
10 | val slackChannel: String,
11 | val threadTs: String,
12 | val message: String,
13 | ) : AsyncRedisPubSubEvent(
14 | channel = RedisPubSubChannel.SLACK_REPLIED,
15 | traceId = runCatching { MDC.get(TRACE_ID) }
16 | .getOrElse { IdGenerator.generate().toString() },
17 | )
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/UserStatisticController.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller
2 |
3 | import org.gitanimals.render.controller.response.TotalUserResponse
4 | import org.gitanimals.render.domain.UserStatisticService
5 | import org.springframework.web.bind.annotation.GetMapping
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @RestController
9 | class UserStatisticController(
10 | private val userStatisticService: UserStatisticService,
11 | ) {
12 |
13 | @GetMapping("/users/statistics/total")
14 | fun totalUsers(): TotalUserResponse =
15 | TotalUserResponse.from(userStatisticService.getTotalUserCount())
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/redis/TransactionCommitRedisPubSubEvent.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.redis
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore
4 | import org.gitanimals.core.filter.MDCFilter
5 | import org.slf4j.MDC
6 | import org.springframework.context.ApplicationEvent
7 |
8 | abstract class TransactionCommitRedisPubSubEvent(
9 | val apiUserId: String? = runCatching { MDC.get(MDCFilter.USER_ID) }.getOrNull(),
10 | val traceId: String,
11 | @JsonIgnore
12 | val channel: String,
13 | source: Any,
14 | ) : ApplicationEvent(source) {
15 |
16 | @JsonIgnore
17 | override fun getSource(): Any {
18 | return super.source
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/KickGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.auth.InternalAuth
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.springframework.stereotype.Service
6 |
7 | @Service
8 | class KickGuildFacade(
9 | private val internalAuth: InternalAuth,
10 | private val guildService: GuildService,
11 | ) {
12 |
13 | fun kickMember(guildId: Long, kickUserId: Long) {
14 | val userId = internalAuth.getUserId()
15 |
16 | guildService.kickMember(
17 | kickerId = userId,
18 | guildId = guildId,
19 | kickUserId = kickUserId,
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain.request
2 |
3 | import org.gitanimals.core.PersonaType
4 | import org.gitanimals.guild.domain.Leader
5 |
6 | data class CreateLeaderRequest(
7 | val userId: Long,
8 | val name: String,
9 | val personaId: Long,
10 | val contributions: Long,
11 | val personaType: PersonaType,
12 | ) {
13 |
14 | fun toDomain(): Leader {
15 | return Leader(
16 | userId = userId,
17 | name = name,
18 | personaId = personaId,
19 | contributions = contributions,
20 | personaType = personaType,
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/response/PersonaEnumResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.response
2 |
3 | import org.gitanimals.core.PersonaGrade
4 | import org.gitanimals.core.PersonaType
5 |
6 | data class PersonaEnumResponse(
7 | val type: PersonaType,
8 | val dropRate: String,
9 | val grade: PersonaGrade,
10 | ) {
11 |
12 | companion object {
13 | fun from(personaType: PersonaType): PersonaEnumResponse {
14 | return PersonaEnumResponse(
15 | type = personaType,
16 | dropRate = personaType.getDropRate(),
17 | grade = personaType.grade,
18 | )
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.auth.InternalAuth
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.springframework.stereotype.Service
6 |
7 | @Service
8 | class AcceptJoinGuildFacade(
9 | private val internalAuth: InternalAuth,
10 | private val guildService: GuildService,
11 | ) {
12 |
13 | fun acceptJoin(guildId: Long, acceptUserId: Long) {
14 | val userId = internalAuth.getUserId()
15 |
16 | guildService.acceptJoin(
17 | acceptorId = userId,
18 | guildId = guildId,
19 | acceptUserId = acceptUserId,
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/play/PlayGround.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.play
2 |
3 | import io.kotest.assertions.throwables.shouldThrow
4 | import io.kotest.core.spec.style.StringSpec
5 | import io.kotest.matchers.shouldBe
6 |
7 | class PlayGround : StringSpec({
8 | "runCatching의 also구문은 runCatching에서 예외가 던져져도 동작한다"() {
9 | var isRunAlso = false
10 | shouldThrow {
11 | runCatching {
12 | throw IllegalArgumentException("hello?")
13 | }.also {
14 | isRunAlso = true
15 | }.getOrElse {
16 | throw it
17 | }
18 | }
19 |
20 | isRunAlso shouldBe true
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/PersonaStatisticController.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller
2 |
3 | import org.gitanimals.render.controller.response.TotalPersonaResponse
4 | import org.gitanimals.render.domain.PersonaStatisticService
5 | import org.springframework.web.bind.annotation.GetMapping
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @RestController
9 | class PersonaStatisticController(
10 | private val personaStatisticService: PersonaStatisticService,
11 | ) {
12 |
13 | @GetMapping("/personas/statistics/total")
14 | fun totalAdaptedPersonas(): TotalPersonaResponse =
15 | TotalPersonaResponse.from(personaStatisticService.getTotalPersonaCount())
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/PersonaStatisticRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import org.springframework.data.domain.Pageable
4 | import org.springframework.data.domain.Slice
5 | import org.springframework.data.jpa.repository.JpaRepository
6 | import org.springframework.data.jpa.repository.Query
7 | import org.springframework.data.repository.query.Param
8 | import java.time.Instant
9 |
10 | interface PersonaStatisticRepository : JpaRepository {
11 |
12 | @Query("select p from persona p where p.createdAt >= :createdAt")
13 | fun findAllPersonaByCreatedAtAfter(
14 | @Param("createdAt") createdAt: Instant,
15 | pageable: Pageable,
16 | ): Slice
17 | }
18 |
--------------------------------------------------------------------------------
/.idea/checkstyle-idea.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 10.10.0
5 | JavaOnly
6 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # LICENSE
2 |
3 | ## Restrictions on Use
4 |
5 | Unauthorized use, reproduction, or distribution of GitAnimals' designs is strictly prohibited.
6 |
7 | All designs, including but not limited to illustrations, icons, logos, and characters, are the exclusive property of the creator and GitAnimals.
8 |
9 | Any commercial or non-commercial use of GitAnimals' designs without prior written permission is not allowed.
10 |
11 | ## Copyright Notice
12 |
13 | All designs and visual assets associated with GitAnimals are copyrighted and belong to their respective creators and GitAnimals. Any violation of these terms may result in legal action.
14 |
15 | For inquiries regarding usage permissions, please contact us at xb@gitanimals.org
16 |
17 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/star/controller/StargazerController.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.star.controller
2 |
3 | import org.gitanimals.star.domain.StargazerService
4 | import org.springframework.web.bind.annotation.GetMapping
5 | import org.springframework.web.bind.annotation.PathVariable
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @RestController
9 | class StargazerController(
10 | private val stargazerService: StargazerService,
11 | ) {
12 |
13 | @GetMapping("/stargazers/{login}/press")
14 | fun isPressStar(@PathVariable("login") login: String): Map {
15 | val isPressStar = stargazerService.existsByLogin(login)
16 |
17 | return mapOf("isPressStar" to isPressStar)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/deadletter/DeadLetterEvent.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.deadletter
2 |
3 | import org.gitanimals.core.IdGenerator
4 | import org.gitanimals.core.filter.MDCFilter
5 | import org.gitanimals.core.redis.AsyncRedisPubSubEvent
6 | import org.gitanimals.core.redis.RedisPubSubChannel.DEAD_LETTER_OCCURRED
7 | import org.slf4j.MDC
8 |
9 | data class DeadLetterEvent(
10 | val deadLetterId: String,
11 | val sagaId: String,
12 | val nodeName: String,
13 | val group: String,
14 | val deadLetter: Map,
15 | ) : AsyncRedisPubSubEvent(
16 | traceId = runCatching { MDC.get(MDCFilter.TRACE_ID) }
17 | .getOrElse { IdGenerator.generate().toString() },
18 | channel = DEAD_LETTER_OCCURRED,
19 | )
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/orchestrate/IdentityApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.orchestrate
2 |
3 | import org.gitanimals.core.auth.UserEntryPoint
4 | import org.springframework.web.bind.annotation.RequestBody
5 | import org.springframework.web.bind.annotation.RequestParam
6 | import org.springframework.web.service.annotation.PatchExchange
7 |
8 | fun interface IdentityApi {
9 |
10 | @PatchExchange("/internals/users")
11 | fun updateUserByAuthInfo(
12 | @RequestParam("entry-point") entryPoint: UserEntryPoint,
13 | @RequestParam("authentication-id") authenticationId: String,
14 | @RequestBody request: UsernameUpdateRequest,
15 | )
16 | }
17 |
18 | data class UsernameUpdateRequest(
19 | val changedName: String,
20 | )
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 |
7 | ### IntelliJ IDEA ###
8 | .idea/modules.xml
9 | .idea/jarRepositories.xml
10 | .idea/compiler.xml
11 | .idea/libraries/
12 | *.iws
13 | *.iml
14 | *.ipr
15 | out/
16 | !**/src/main/**/out/
17 | !**/src/test/**/out/
18 |
19 | ### Eclipse ###
20 | .apt_generated
21 | .classpath
22 | .factorypath
23 | .project
24 | .settings
25 | .springBeans
26 | .sts4-cache
27 | bin/
28 | !**/src/main/**/bin/
29 | !**/src/test/**/bin/
30 |
31 | ### NetBeans ###
32 | /nbproject/private/
33 | /nbbuild/
34 | /dist/
35 | /nbdist/
36 | /.nb-gradle/
37 |
38 | ### VS Code ###
39 | .vscode/
40 |
41 | ### Mac OS ###
42 | .DS_Store
43 |
44 | .idea
45 | logs
46 | .profileconfig.json
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/infra/PersonaDeletedEventHandler.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.infra
2 |
3 | import org.gitanimals.render.domain.event.PersonaDeleted
4 | import org.gitanimals.render.infra.CustomExecutorConfigurer.Companion.GRACEFUL_SHUTDOWN_EXECUTOR
5 | import org.rooftop.netx.api.SagaManager
6 | import org.springframework.context.event.EventListener
7 | import org.springframework.scheduling.annotation.Async
8 | import org.springframework.stereotype.Component
9 |
10 | @Component
11 | class PersonaDeletedEventHandler(
12 | private val sagaManager: SagaManager,
13 | ) {
14 |
15 | @Async(GRACEFUL_SHUTDOWN_EXECUTOR)
16 | @EventListener
17 | fun handlePersonaDeletedEvent(personaDeleted: PersonaDeleted) {
18 | sagaManager.startSync(personaDeleted)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/infra/event/NewPetDropRateDistributionEvent.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.infra.event
2 |
3 | import org.gitanimals.core.filter.MDCFilter.Companion.TRACE_ID
4 | import org.gitanimals.core.redis.AsyncRedisPubSubEvent
5 | import org.gitanimals.core.redis.RedisPubSubChannel.NEW_PET_DROP_RATE_DISTRIBUTION
6 | import org.slf4j.MDC
7 |
8 | data class NewPetDropRateDistributionEvent(
9 | val type: Type,
10 | val distributions: List,
11 | ) : AsyncRedisPubSubEvent(
12 | traceId = MDC.get(TRACE_ID),
13 | channel = NEW_PET_DROP_RATE_DISTRIBUTION,
14 | ) {
15 | data class Distribution(
16 | val dropRate: Double,
17 | val count: Int,
18 | )
19 | }
20 |
21 | enum class Type {
22 | DAILY,
23 | WEEKLY,
24 | MONTHLY,
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/deadletter/DeadLetterListenConfigurer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.deadletter
2 |
3 | import org.rooftop.netx.spi.DeadLetterRegistry
4 | import org.springframework.context.ApplicationEventPublisher
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.context.annotation.Configuration
7 |
8 | @Configuration
9 | class DeadLetterListenConfigurer(
10 | private val deadLetterRegistry: DeadLetterRegistry,
11 | private val applicationEventPublisher: ApplicationEventPublisher,
12 | ) {
13 |
14 | @Bean
15 | fun deadLetterEventPublisher(): DeadLetterEventPublisher {
16 | return DeadLetterEventPublisher(applicationEventPublisher).apply {
17 | deadLetterRegistry.addListener(this)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/UserContributionRankRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository
4 | import org.springframework.data.jpa.repository.Modifying
5 | import org.springframework.data.jpa.repository.Query
6 | import org.springframework.data.repository.query.Param
7 |
8 | interface UserContributionRankRepository : JpaRepository {
9 | fun findByUserId(userId: Long): UserContributionRank?
10 |
11 | @Query("select u from UserContributionRank as u where u.username = :username")
12 | fun findByUsername(@Param("username") username: String): UserContributionRank?
13 |
14 | @Modifying
15 | @Query("update UserContributionRank as u set u.weeklyContributions = 0")
16 | fun initialWeeklyRanks()
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/RankQueryRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | interface RankQueryRepository {
4 |
5 | fun findAllRank(rankStartedAt: Int, rankType: RankType, limit: Int): Set
6 |
7 | fun getRankByRankId(rankType: RankType, rankId: Long): RankQueryResponse
8 |
9 | fun updateRank(rankType: RankType, rankId: RankId, score: Long)
10 |
11 | fun initialRank(rankType: RankType)
12 |
13 | fun getTotalRankCount(rankType: RankType): Int
14 |
15 | enum class RankType {
16 | WEEKLY_GUILD_CONTRIBUTIONS,
17 | WEEKLY_USER_CONTRIBUTIONS,
18 | ;
19 | }
20 |
21 | data class RankQueryResponse(
22 | val id: Long,
23 | val rank: Int,
24 | )
25 |
26 | }
27 |
28 | @JvmInline
29 | value class RankId(val value: Long)
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/interceptor/InternalApiInterceptor.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.interceptor
2 |
3 | import jakarta.servlet.http.HttpServletRequest
4 | import jakarta.servlet.http.HttpServletResponse
5 | import org.springframework.web.servlet.HandlerInterceptor
6 |
7 | class InternalApiInterceptor(
8 | private val internalSecret: String,
9 | ) : HandlerInterceptor {
10 |
11 | override fun preHandle(
12 | request: HttpServletRequest,
13 | response: HttpServletResponse,
14 | handler: Any,
15 | ): Boolean {
16 | val requestSecret = request.getHeader("Internal-Secret")
17 |
18 | if (requestSecret == internalSecret) {
19 | return true
20 | }
21 |
22 | response.sendError(403, "Cannot call internal API")
23 | return false
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/gradle/spring.gradle:
--------------------------------------------------------------------------------
1 | jar {
2 | enabled = false
3 | }
4 |
5 | dependencies {
6 | implementation "org.springframework.boot:spring-boot-starter"
7 | implementation "org.springframework.boot:spring-boot-starter-web"
8 | implementation "org.springframework.boot:spring-boot-starter-aop"
9 | implementation "org.springframework.boot:spring-boot-starter-cache"
10 | implementation "org.springframework.boot:spring-boot-starter-data-jpa"
11 | implementation "org.springframework.boot:spring-boot-starter-actuator"
12 | implementation "org.springframework.boot:spring-boot-starter-data-redis"
13 | implementation 'org.springframework.retry:spring-retry'
14 |
15 | testImplementation "org.springframework.boot:spring-boot-starter-test"
16 |
17 | testImplementation "com.ninja-squad:springmockk:${springMockkVersion}"
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/extension/StringExtension.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.extension
2 |
3 | object StringExtension {
4 |
5 | fun String.trimNotDigitCharacters(): String {
6 | var trimed = this.trim()
7 |
8 | if (trimed.first().isDigit().not()) {
9 | trimed = trimed.drop(1)
10 | }
11 | if (trimed.last().isDigit().not()) {
12 | trimed = trimed.dropLast(1)
13 | }
14 |
15 | return trimed
16 | }
17 |
18 | fun String.deleteBrackets(): String {
19 | val start = when (this[0]) {
20 | '{' -> 1
21 | else -> 0
22 | }
23 |
24 | val end = when (this.last()) {
25 | '}' -> this.length - 1
26 | else -> this.length
27 | }
28 |
29 | return this.substring(start, end)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/ChangeGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.auth.InternalAuth
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.gitanimals.guild.domain.request.ChangeGuildRequest
6 | import org.springframework.stereotype.Service
7 |
8 | @Service
9 | class ChangeGuildFacade(
10 | private val internalAuth: InternalAuth,
11 | private val guildService: GuildService,
12 | ) {
13 |
14 | fun changeGuild(guildId: Long, changeGuildRequest: ChangeGuildRequest) {
15 | changeGuildRequest.requireValidTitle()
16 |
17 | val userId = internalAuth.getUserId()
18 |
19 | guildService.changeGuild(
20 | changeRequesterId = userId,
21 | guildId = guildId,
22 | request = changeGuildRequest,
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/ratelimit/RateLimitable.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.ratelimit
2 |
3 | import org.gitanimals.core.instant
4 | import java.time.Instant
5 | import java.time.LocalDateTime
6 |
7 | interface RateLimitable {
8 |
9 | fun acquire(limitPercent: Double = 0.0, action: suspend (RateLimit?) -> T): T
10 |
11 | fun findRateLimit(): RateLimit?
12 |
13 | fun update(rateLimit: RateLimit)
14 |
15 | data class RateLimit(
16 | val limit: Int,
17 | val remaining: Int,
18 | val resetAt: LocalDateTime,
19 | val used: Int,
20 | val requestedAt: Instant = instant(),
21 | ) {
22 | fun getRemainPercentage(): Double {
23 | val percentage = (remaining.toDouble() / limit.toDouble()) * 100.0
24 | return percentage.coerceIn(0.0, 100.0)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/slack/SlackSender.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.slack
2 |
3 | import com.slack.api.Slack
4 | import com.slack.api.methods.MethodsClient
5 | import com.slack.api.methods.request.chat.ChatPostMessageRequest
6 | import org.springframework.beans.factory.annotation.Value
7 | import org.springframework.stereotype.Component
8 |
9 | @Component
10 | class SlackSender(
11 | @Value("\${slack.token}") slackToken: String,
12 | ) {
13 |
14 | private val slack: MethodsClient by lazy {
15 | Slack.getInstance().methods(slackToken)
16 | }
17 |
18 | fun send(channel: String, message: String) {
19 | val request: ChatPostMessageRequest = ChatPostMessageRequest.builder()
20 | .channel(channel)
21 | .text(message)
22 | .build()
23 |
24 | slack.chatPostMessage(request)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/render/supports/DomainEventHolder.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.supports
2 |
3 | import io.kotest.matchers.shouldBe
4 | import org.gitanimals.render.domain.event.PersonaDeleted
5 | import org.springframework.boot.test.context.TestComponent
6 | import org.springframework.context.event.EventListener
7 | import kotlin.reflect.KClass
8 |
9 | @TestComponent
10 | class DomainEventHolder {
11 |
12 | private val events = mutableMapOf, Int>()
13 |
14 | @EventListener(PersonaDeleted::class)
15 | fun handlePersonaDeleted(personaDeleted: PersonaDeleted) {
16 | events[personaDeleted::class] = events.getOrDefault(personaDeleted::class, 0) + 1
17 | }
18 |
19 | fun eventsShouldBe(kClass: KClass<*>, count: Int) {
20 | events[kClass] shouldBe count
21 | }
22 |
23 | fun deleteAll() = events.clear()
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/controller/response/BackgroundResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.controller.response
2 |
3 | import org.gitanimals.core.FieldType
4 | import org.gitanimals.render.domain.User
5 |
6 | data class BackgroundResponse(
7 | val id: String,
8 | val name: String,
9 | val backgrounds: List,
10 | ) {
11 |
12 | data class Background(
13 | val type: FieldType,
14 | )
15 |
16 | companion object {
17 | fun from(user: User): BackgroundResponse {
18 | return BackgroundResponse(
19 | id = user.id.toString(),
20 | name = user.getName(),
21 | backgrounds = user.fields.map { Background(it.fieldType) }.ifEmpty {
22 | listOf(Background(FieldType.WHITE_FIELD))
23 | },
24 | )
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/app/RenderApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.app
2 |
3 | import org.gitanimals.core.PersonaType
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.service.annotation.GetExchange
6 |
7 | fun interface RenderApi {
8 |
9 | @GetExchange("/internals/users/{username}/top-level-personas")
10 | fun getUserWithTopLevelPersona(@PathVariable("username") username: String): UserResponse
11 |
12 | data class UserResponse(
13 | val id: String,
14 | val name: String,
15 | val totalContributions: String,
16 | val personas: List,
17 | )
18 |
19 | data class PersonaResponse(
20 | val id: String,
21 | val type: PersonaType,
22 | val level: String,
23 | val visible: Boolean,
24 | val dropRate: String,
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/UserStatisticService.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import org.springframework.cache.annotation.Cacheable
4 | import org.springframework.stereotype.Service
5 | import java.time.LocalDate
6 | import java.time.ZoneOffset
7 |
8 | @Service
9 | class UserStatisticService(
10 | private val userStatisticRepository: UserStatisticRepository,
11 | ) {
12 |
13 | fun getYesterdayUserCount(): Int {
14 | val yesterday = LocalDate.now().minusDays(1)
15 | val startDay = yesterday.atTime(0, 0, 0).toInstant(ZoneOffset.UTC)
16 | val endDay = yesterday.atTime(23, 59, 59).toInstant(ZoneOffset.UTC)
17 | return userStatisticRepository.getDailyUserCount(startDay, endDay)
18 | }
19 |
20 | @Cacheable(value = ["total_user_count_cache"])
21 | fun getTotalUserCount(): Long = userStatisticRepository.count()
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Embeddable
5 | import jakarta.persistence.EnumType
6 | import jakarta.persistence.Enumerated
7 | import org.gitanimals.core.PersonaType
8 |
9 | @Embeddable
10 | data class Leader(
11 | @Column(name = "leader_id", nullable = false)
12 | val userId: Long,
13 |
14 | @Column(name = "name", nullable = false, columnDefinition = "VARCHAR(255)")
15 | var name: String,
16 |
17 | @Column(name = "persona_id", nullable = false)
18 | var personaId: Long,
19 |
20 | @Enumerated(EnumType.STRING)
21 | @Column(name = "persona_type", nullable = false, columnDefinition = "VARCHAR(255)")
22 | var personaType: PersonaType,
23 |
24 | @Column(name = "contributions", nullable = false)
25 | var contributions: Long,
26 | )
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/infra/RankCacheConfigurer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.infra
2 |
3 | import com.github.benmanes.caffeine.cache.Caffeine
4 | import org.springframework.cache.CacheManager
5 | import org.springframework.cache.caffeine.CaffeineCacheManager
6 | import org.springframework.context.annotation.Bean
7 | import org.springframework.context.annotation.Configuration
8 | import kotlin.time.Duration.Companion.minutes
9 | import kotlin.time.toJavaDuration
10 |
11 | @Configuration
12 | class RankCacheConfigurer {
13 |
14 | @Bean
15 | fun rankTotalCountCacheManager(): CacheManager {
16 | val caffeineConfig = Caffeine.newBuilder().expireAfterWrite(10.minutes.toJavaDuration())
17 |
18 | val caffeineCacheManager = CaffeineCacheManager()
19 | caffeineCacheManager.setCaffeine(caffeineConfig)
20 |
21 | return caffeineCacheManager
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/guild/supports/RedisContainer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.supports
2 |
3 |
4 | import org.springframework.boot.test.context.TestConfiguration
5 | import org.testcontainers.containers.GenericContainer
6 | import org.testcontainers.utility.DockerImageName
7 |
8 | @TestConfiguration
9 | internal class RedisContainer {
10 | init {
11 | val redis: GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:7.2.3"))
12 | .withExposedPorts(6379)
13 |
14 | runCatching {
15 | redis.start()
16 | }.onFailure {
17 | if (it is com.github.dockerjava.api.exception.InternalServerErrorException) {
18 | redis.start()
19 | }
20 | }
21 |
22 | System.setProperty(
23 | "netx.port",
24 | redis.getMappedPort(6379).toString()
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/GuildIcons.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain
2 |
3 | enum class GuildIcons(
4 | private val imageName: String,
5 | ) {
6 | CAT("GUILD-1"),
7 | CHICK("GUILD-2"),
8 | FLAMINGO("GUILD-3"),
9 | RABBIT("GUILD-4"),
10 | DESSERT_FOX("GUILD-5"),
11 | GHOST("GUILD-6"),
12 | HAMSTER("GUILD-7"),
13 | SLIME("GUILD-8"),
14 | PIG("GUILD-9"),
15 | PENGUIN("GUILD-10"),
16 | ;
17 |
18 | fun getImagePath() = "$IMAGE_PATH_PREFIX$imageName"
19 |
20 | companion object {
21 | private const val IMAGE_PATH_PREFIX = "https://static.gitanimals.org/guilds/icons/"
22 |
23 | fun requireExistImagePath(imagePath: String) {
24 | require(entries.any { it.getImagePath() == imagePath }) {
25 | "Cannot find matched image by imagePath \"$imagePath\""
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/render/supports/RedisContainer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.supports
2 |
3 |
4 | import org.springframework.boot.test.context.TestConfiguration
5 | import org.testcontainers.containers.GenericContainer
6 | import org.testcontainers.utility.DockerImageName
7 |
8 | @TestConfiguration
9 | internal class RedisContainer {
10 | init {
11 | val redis: GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:7.2.3"))
12 | .withExposedPorts(6379)
13 |
14 | runCatching {
15 | redis.start()
16 | }.onFailure {
17 | if (it is com.github.dockerjava.api.exception.InternalServerErrorException) {
18 | redis.start()
19 | }
20 | }
21 |
22 | System.setProperty(
23 | "netx.port",
24 | redis.getMappedPort(6379).toString()
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/event/GuildContributionUpdated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain.event
2 |
3 | import org.gitanimals.core.DomainEventPublisher
4 | import org.gitanimals.core.IdGenerator
5 | import org.gitanimals.core.filter.MDCFilter
6 | import org.gitanimals.core.redis.RedisPubSubChannel
7 | import org.gitanimals.core.redis.TransactionCommitRedisPubSubEvent
8 | import org.slf4j.MDC
9 |
10 | data class GuildContributionUpdated(
11 | val guildId: Long,
12 | val guildTitle: String,
13 | val guildImage: String,
14 | val contributions: Long,
15 | val updatedContributions: Long,
16 | ) : TransactionCommitRedisPubSubEvent(
17 | traceId = runCatching { MDC.get(MDCFilter.TRACE_ID) }.getOrElse {
18 | IdGenerator.generate().toString()
19 | },
20 | channel = RedisPubSubChannel.GUILD_CONTRIBUTION_UPDATED,
21 | source = DomainEventPublisher::class,
22 | )
23 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/render/supports/IntegrationTest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.supports
2 |
3 | import org.springframework.boot.autoconfigure.domain.EntityScan
4 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
5 | import org.springframework.core.annotation.AliasFor
6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories
7 | import org.springframework.test.context.ContextConfiguration
8 | import org.springframework.test.context.event.RecordApplicationEvents
9 | import kotlin.reflect.KClass
10 |
11 | @DataJpaTest
12 | @ContextConfiguration
13 | @RecordApplicationEvents
14 | @EntityScan(basePackages = ["org.gitanimals.render.domain"])
15 | @EnableJpaRepositories(basePackages = ["org.gitanimals.render.domain"])
16 | annotation class IntegrationTest(
17 | @get:AliasFor(annotation = ContextConfiguration::class, value = "classes")
18 | val classes: Array>,
19 | )
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/Application.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals
2 |
3 | import org.rooftop.netx.meta.EnableSaga
4 | import org.springframework.boot.SpringApplication
5 | import org.springframework.boot.autoconfigure.SpringBootApplication
6 | import org.springframework.cache.annotation.EnableCaching
7 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing
8 | import org.springframework.retry.annotation.EnableRetry
9 | import org.springframework.scheduling.annotation.EnableAsync
10 | import org.springframework.scheduling.annotation.EnableScheduling
11 |
12 | @EnableSaga
13 | @EnableRetry
14 | @EnableAsync
15 | @EnableCaching
16 | @EnableScheduling
17 | @EnableJpaAuditing
18 | @SpringBootApplication
19 | class Application {
20 |
21 | companion object {
22 |
23 | @JvmStatic
24 | fun main(args: Array) {
25 | SpringApplication.run(Application::class.java, *args)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/star/domain/StargazerService.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.star.domain
2 |
3 | import jakarta.persistence.EntityManager
4 | import org.springframework.cache.annotation.Cacheable
5 | import org.springframework.stereotype.Service
6 | import org.springframework.transaction.annotation.Transactional
7 |
8 | @Service
9 | @Transactional(readOnly = true)
10 | class StargazerService(
11 | private val stargazersRepository: StargazersRepository,
12 | private val entityManager: EntityManager,
13 | ) {
14 |
15 | @Cacheable(value = ["exists_by_login_cache"])
16 | fun existsByLogin(login: String): Boolean = stargazersRepository.existsById(login)
17 |
18 | @Transactional
19 | fun updateAll(logins: List) {
20 | stargazersRepository.deleteAllInBatch()
21 |
22 | logins.map { entityManager.persist(Stargazer(it)) }
23 | entityManager.flush()
24 | entityManager.clear()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/interceptor/InterceptorConfigurer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.interceptor
2 |
3 | import org.springframework.beans.factory.annotation.Value
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry
7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
8 |
9 | @Configuration
10 | class InterceptorConfigurer(
11 | @Value("\${internal.secret}") private val internalSecret: String,
12 | ) : WebMvcConfigurer {
13 |
14 | override fun addInterceptors(registry: InterceptorRegistry) {
15 | registry.addInterceptor(internalApiInterceptor())
16 | .addPathPatterns("/internals/**")
17 | .excludePathPatterns()
18 | }
19 |
20 | @Bean
21 | fun internalApiInterceptor(): InternalApiInterceptor = InternalApiInterceptor(internalSecret)
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/infra/CacheConfigurer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.infra
2 |
3 | import com.github.benmanes.caffeine.cache.Caffeine
4 | import org.springframework.cache.CacheManager
5 | import org.springframework.cache.caffeine.CaffeineCacheManager
6 | import org.springframework.context.annotation.Bean
7 | import org.springframework.context.annotation.Configuration
8 | import org.springframework.context.annotation.Primary
9 | import kotlin.time.Duration.Companion.hours
10 | import kotlin.time.toJavaDuration
11 |
12 | @Configuration
13 | class CacheConfigurer {
14 |
15 | @Bean
16 | @Primary
17 | fun cacheManager(caffeine: Caffeine): CacheManager {
18 | val caffeineCacheManager = CaffeineCacheManager()
19 | caffeineCacheManager.setCaffeine(caffeine)
20 | return caffeineCacheManager
21 | }
22 |
23 | @Bean
24 | fun caffeineConfig(): Caffeine =
25 | Caffeine.newBuilder().expireAfterWrite(1.hours.toJavaDuration())
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/HttpClientErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import org.springframework.http.HttpStatus
4 | import org.springframework.http.client.ClientHttpResponse
5 | import org.springframework.web.client.ResponseErrorHandler
6 |
7 | class HttpClientErrorHandler : ResponseErrorHandler {
8 |
9 | override fun hasError(response: ClientHttpResponse): Boolean {
10 | return response.statusCode.isError
11 | }
12 |
13 | override fun handleError(response: ClientHttpResponse) {
14 | val body = response.body.bufferedReader().use { it.readText() }
15 | when {
16 | response.statusCode.isSameCodeAs(HttpStatus.UNAUTHORIZED) ->
17 | throw AuthorizationException(body)
18 |
19 | response.statusCode.is4xxClientError ->
20 | throw IllegalArgumentException(body)
21 |
22 | response.statusCode.is5xxServerError ->
23 | throw IllegalStateException(body)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/app/IdentityApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.app
2 |
3 | import org.springframework.web.bind.annotation.PathVariable
4 | import org.springframework.web.bind.annotation.RequestParam
5 | import org.springframework.web.service.annotation.GetExchange
6 | import org.springframework.web.service.annotation.PostExchange
7 |
8 | interface IdentityApi {
9 |
10 | @GetExchange("/internals/users/by-name/{name}")
11 | fun getUserByName(@PathVariable("name") name: String): UserResponse
12 |
13 | @PostExchange("/internals/users/points/increases/by-username/{username}")
14 | fun increaseUserPointsByUsername(
15 | @PathVariable("username") username: String,
16 | @RequestParam("point") point: Int,
17 | @RequestParam("idempotency-key") idempotencyKey: String,
18 | )
19 |
20 | data class UserResponse(
21 | val id: String,
22 | val username: String,
23 | val points: String,
24 | val profileImage: String,
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/PersonaEvolutionFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app
2 |
3 | import org.gitanimals.render.domain.UserService
4 | import org.gitanimals.render.domain.response.PersonaResponse
5 | import org.springframework.stereotype.Service
6 |
7 | @Service
8 | class PersonaEvolutionFacade(
9 | private val identityApi: IdentityApi,
10 | private val userService: UserService,
11 | ) {
12 |
13 | fun evolutionPersona(token: String, personaId: Long): PersonaResponse {
14 | val user = identityApi.getUserByToken(token)
15 |
16 | return userService.evolutionPersona(
17 | name = user.username,
18 | personaId = personaId,
19 | )
20 | }
21 |
22 | fun isEvoluationable(token: String, personaId: Long): Boolean {
23 | val user = identityApi.getUserByToken(token)
24 |
25 | return userService.isEvoluationable(
26 | name = user.username,
27 | personaId = personaId,
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/event/UserContributionUpdated.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.event
2 |
3 | import org.gitanimals.core.DomainEventPublisher
4 | import org.gitanimals.core.IdGenerator
5 | import org.gitanimals.core.filter.MDCFilter
6 | import org.gitanimals.core.instant
7 | import org.gitanimals.core.redis.RedisPubSubChannel
8 | import org.gitanimals.core.redis.TransactionCommitRedisPubSubEvent
9 | import org.slf4j.MDC
10 | import java.time.Instant
11 |
12 | data class UserContributionUpdated(
13 | val username: String,
14 | val contributions: Long,
15 | val updatedContributions: Long,
16 | val userContributionUpdated: Boolean = true,
17 | val contributionUpdatedAt: Instant = instant(),
18 | ) : TransactionCommitRedisPubSubEvent(
19 | traceId = runCatching { MDC.get(MDCFilter.TRACE_ID) }.getOrElse {
20 | IdGenerator.generate().toString()
21 | },
22 | channel = RedisPubSubChannel.USER_CONTRIBUTION_UPDATED,
23 | source = DomainEventPublisher::class,
24 | )
25 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/schedule/SchedulerTraceIdAspect.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.schedule
2 |
3 | import org.aspectj.lang.ProceedingJoinPoint
4 | import org.aspectj.lang.annotation.Around
5 | import org.aspectj.lang.annotation.Aspect
6 | import org.gitanimals.core.IdGenerator
7 | import org.gitanimals.core.filter.MDCFilter.Companion.TRACE_ID
8 | import org.slf4j.MDC
9 | import org.springframework.core.annotation.Order
10 | import org.springframework.stereotype.Component
11 |
12 | @Aspect
13 | @Component
14 | @Order(value = Int.MIN_VALUE)
15 | class SchedulerTraceIdAspect {
16 |
17 | @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)")
18 | fun putMdcFilter(joinPoint: ProceedingJoinPoint): Any? {
19 | return runCatching {
20 | MDC.put(TRACE_ID, IdGenerator.generate().toString())
21 | joinPoint.proceed()
22 | }.also {
23 | MDC.remove(TRACE_ID)
24 | }.onFailure {
25 | throw it
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/AbstractTime.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.EntityListeners
5 | import jakarta.persistence.MappedSuperclass
6 | import jakarta.persistence.PrePersist
7 | import org.springframework.data.annotation.CreatedDate
8 | import org.springframework.data.annotation.LastModifiedDate
9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener
10 | import java.time.Instant
11 |
12 | @MappedSuperclass
13 | @EntityListeners(AuditingEntityListener::class)
14 | abstract class AbstractTime(
15 | @CreatedDate
16 | @Column(name = "created_at")
17 | var createdAt: Instant = Instant.now(),
18 |
19 | @LastModifiedDate
20 | @Column(name = "modified_at")
21 | var modifiedAt: Instant? = null,
22 | ) {
23 |
24 | @PrePersist
25 | fun prePersist() {
26 | modifiedAt = when (modifiedAt == null) {
27 | true -> createdAt
28 | false -> return
29 | }
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/AbstractTime.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.EntityListeners
5 | import jakarta.persistence.MappedSuperclass
6 | import jakarta.persistence.PrePersist
7 | import org.springframework.data.annotation.CreatedDate
8 | import org.springframework.data.annotation.LastModifiedDate
9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener
10 | import java.time.Instant
11 |
12 | @MappedSuperclass
13 | @EntityListeners(AuditingEntityListener::class)
14 | abstract class AbstractTime(
15 | @CreatedDate
16 | @Column(name = "created_at")
17 | var createdAt: Instant = Instant.now(),
18 |
19 | @LastModifiedDate
20 | @Column(name = "modified_at")
21 | var modifiedAt: Instant? = null,
22 | ) {
23 |
24 | @PrePersist
25 | fun prePersist() {
26 | modifiedAt = when (modifiedAt == null) {
27 | true -> createdAt
28 | false -> return
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/AbstractTime.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.EntityListeners
5 | import jakarta.persistence.MappedSuperclass
6 | import jakarta.persistence.PrePersist
7 | import org.springframework.data.annotation.CreatedDate
8 | import org.springframework.data.annotation.LastModifiedDate
9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener
10 | import java.time.Instant
11 |
12 | @MappedSuperclass
13 | @EntityListeners(AuditingEntityListener::class)
14 | abstract class AbstractTime(
15 | @CreatedDate
16 | @Column(name = "created_at")
17 | var createdAt: Instant = Instant.now(),
18 |
19 | @LastModifiedDate
20 | @Column(name = "modified_at")
21 | var modifiedAt: Instant? = null,
22 | ) {
23 |
24 | @PrePersist
25 | fun prePersist() {
26 | modifiedAt = when (modifiedAt == null) {
27 | true -> createdAt
28 | false -> return
29 | }
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/DomainEventPublisher.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import org.slf4j.LoggerFactory
4 | import org.springframework.context.ApplicationEventPublisher
5 | import org.springframework.stereotype.Component
6 |
7 | object DomainEventPublisher {
8 |
9 | private lateinit var applicationEventPublisher: ApplicationEventPublisher
10 | private val logger = LoggerFactory.getLogger(this::class.simpleName)
11 |
12 | fun publish(event: T) {
13 | runCatching {
14 | applicationEventPublisher.publishEvent(event)
15 | }.onSuccess {
16 | logger.info("Publish event success. event: \"$event\"")
17 | }.onFailure {
18 | logger.error("Publish event fail. event: \"$event\"")
19 | }
20 | }
21 |
22 | @Component
23 | class EventPublisherInjector(applicationEventPublisher: ApplicationEventPublisher) {
24 |
25 | init {
26 | DomainEventPublisher.applicationEventPublisher = applicationEventPublisher
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/domain/SearchFilter.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.domain
2 |
3 | enum class SearchFilter {
4 |
5 | RANDOM {
6 | override fun sort(guilds: List) = guilds
7 | },
8 |
9 | PEOPLE_ASC {
10 | override fun sort(guilds: List): List {
11 | return guilds.sortedBy { it.getMembers().size }
12 | }
13 | },
14 |
15 | PEOPLE_DESC {
16 | override fun sort(guilds: List): List {
17 | return guilds.sortedByDescending { it.getMembers().size }
18 |
19 | }
20 | },
21 |
22 | CONTRIBUTION_ASC {
23 | override fun sort(guilds: List): List {
24 | return guilds.sortedBy { it.getTotalContributions() }
25 | }
26 | },
27 |
28 | CONTRIBUTION_DESC {
29 | override fun sort(guilds: List): List {
30 | return guilds.sortedByDescending { it.getTotalContributions() }
31 | }
32 | },
33 | ;
34 |
35 | abstract fun sort(guilds: List): List
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/infra/HttpClientErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.infra
2 |
3 | import org.gitanimals.core.AuthorizationException
4 | import org.springframework.http.HttpStatus
5 | import org.springframework.http.client.ClientHttpResponse
6 | import org.springframework.web.client.ResponseErrorHandler
7 |
8 |
9 | class HttpClientErrorHandler : ResponseErrorHandler {
10 |
11 | override fun hasError(response: ClientHttpResponse): Boolean {
12 | return response.statusCode.isError
13 | }
14 |
15 | override fun handleError(response: ClientHttpResponse) {
16 | val body = response.body.bufferedReader().use { it.readText() }
17 | when {
18 | response.statusCode.isSameCodeAs(HttpStatus.UNAUTHORIZED) ->
19 | throw AuthorizationException(body)
20 |
21 | response.statusCode.is4xxClientError ->
22 | throw IllegalArgumentException(body)
23 |
24 | response.statusCode.is5xxServerError ->
25 | throw IllegalStateException(body)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/infra/HttpClientErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.infra
2 |
3 | import org.gitanimals.core.AuthorizationException
4 | import org.springframework.http.HttpStatus
5 | import org.springframework.http.client.ClientHttpResponse
6 | import org.springframework.web.client.ResponseErrorHandler
7 |
8 |
9 | class HttpClientErrorHandler : ResponseErrorHandler {
10 |
11 | override fun hasError(response: ClientHttpResponse): Boolean {
12 | return response.statusCode.isError
13 | }
14 |
15 | override fun handleError(response: ClientHttpResponse) {
16 | val body = response.body.bufferedReader().use { it.readText() }
17 | when {
18 | response.statusCode.isSameCodeAs(HttpStatus.UNAUTHORIZED) ->
19 | throw AuthorizationException(body)
20 |
21 | response.statusCode.is4xxClientError ->
22 | throw IllegalArgumentException(body)
23 |
24 | response.statusCode.is5xxServerError ->
25 | throw IllegalStateException(body)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/infra/CustomExecutorConfigurer.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.infra
2 |
3 | import org.springframework.context.annotation.Bean
4 | import org.springframework.context.annotation.Configuration
5 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
6 | import java.util.concurrent.Executor
7 | import kotlin.time.Duration.Companion.minutes
8 |
9 | @Configuration
10 | class CustomExecutorConfigurer {
11 |
12 | @Bean(GRACEFUL_SHUTDOWN_EXECUTOR)
13 | fun taskExecutor(): Executor {
14 | val executor = ThreadPoolTaskExecutor()
15 | executor.corePoolSize = 2
16 | executor.maxPoolSize = 20
17 | executor.threadNamePrefix = "$GRACEFUL_SHUTDOWN_EXECUTOR-"
18 | executor.setWaitForTasksToCompleteOnShutdown(true)
19 | executor.setAwaitTerminationSeconds(2.minutes.inWholeSeconds.toInt())
20 | executor.initialize()
21 |
22 | return executor
23 | }
24 |
25 | companion object {
26 | const val GRACEFUL_SHUTDOWN_EXECUTOR = "gracefulShutdownExecutor"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/ChangeMainPersonaFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.guild.domain.GuildService
4 | import org.springframework.stereotype.Component
5 |
6 | @Component
7 | class ChangeMainPersonaFacade(
8 | private val renderApi: RenderApi,
9 | private val identityApi: IdentityApi,
10 | private val guildService: GuildService,
11 | ) {
12 |
13 | fun changeMainPersona(token: String, guildId: Long, personaId: Long) {
14 | val user = identityApi.getUserByToken(token)
15 | val personas = renderApi.getUserByName(user.username).personas
16 |
17 | val changedPersona = personas.firstOrNull { it.id.toLong() == personaId }
18 | ?: throw IllegalArgumentException("Cannot change persona to \"$personaId\" from user \"${user.username}\"")
19 |
20 | guildService.changeMainPersona(
21 | guildId = guildId,
22 | userId = user.id.toLong(),
23 | personaId = changedPersona.id.toLong(),
24 | personaType = changedPersona.type,
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/SearchGuildFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.guild.domain.Guild
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.gitanimals.guild.domain.RandomGuildCache
6 | import org.gitanimals.guild.domain.SearchFilter
7 | import org.springframework.data.domain.Page
8 | import org.springframework.stereotype.Service
9 |
10 | @Service
11 | class SearchGuildFacade(
12 | private val randomGuildCache: RandomGuildCache,
13 | private val guildService: GuildService,
14 | ) {
15 |
16 | fun search(key: Int, text: String, pageNumber: Int, filter: SearchFilter): Page {
17 | if (filter == SearchFilter.RANDOM) {
18 | return randomGuildCache.get(
19 | key = key,
20 | text = text,
21 | pageNumber = pageNumber,
22 | filter = filter,
23 | )
24 | }
25 |
26 | return guildService.search(
27 | text = text,
28 | pageNumber = pageNumber,
29 | filter = filter,
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/core/extension/StringExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.extension
2 |
3 | import io.kotest.core.annotation.DisplayName
4 | import io.kotest.core.spec.style.DescribeSpec
5 | import io.kotest.matchers.shouldBe
6 | import org.gitanimals.core.extension.StringExtension.deleteBrackets
7 | import org.gitanimals.core.extension.StringExtension.trimNotDigitCharacters
8 |
9 | @DisplayName("StringExtension class의")
10 | internal class StringExtensionTest : DescribeSpec({
11 |
12 | describe("trimNotDigitCharacters 메소드는") {
13 | context("입력받은 문자의 앞 뒤로 숫자가 아닌 문자가 있다면") {
14 | val text = "\"12345\""
15 |
16 | it("앞 뒤 문자를 삭제한다.") {
17 | val result = text.trimNotDigitCharacters()
18 |
19 | result shouldBe "12345"
20 | }
21 | }
22 | }
23 |
24 | describe("deleteBrackets 메소드는") {
25 | val text = "{devxb}"
26 | context("입력받은 문자의 앞 뒤로 bracket이 있다면, 삭제한다") {
27 | val result = text.deleteBrackets()
28 |
29 | result shouldBe "devxb"
30 | }
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/saga/UsedCouponSagaHandlers.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.saga
2 |
3 | import org.gitanimals.render.domain.UserService
4 | import org.gitanimals.render.saga.event.CouponUsed
5 | import org.rooftop.netx.api.SagaCommitEvent
6 | import org.rooftop.netx.api.SagaCommitListener
7 | import org.rooftop.netx.meta.SagaHandler
8 |
9 | @SagaHandler
10 | class UsedCouponSagaHandlers(
11 | private val userService: UserService,
12 | ) {
13 |
14 | @SagaCommitListener(event = CouponUsed::class)
15 | fun handleCouponUsedCommitEvent(sagaCommitEvent: SagaCommitEvent) {
16 | val couponUsed = sagaCommitEvent.decodeEvent(CouponUsed::class)
17 | if ((couponUsed.code in acceptableCouponCodes).not()) {
18 | return
19 | }
20 | sagaCommitEvent.setNextEvent(couponUsed)
21 |
22 | userService.givePersonaByCoupon(couponUsed.username, couponUsed.dynamic, couponUsed.code)
23 | }
24 |
25 | private companion object {
26 | private val acceptableCouponCodes = listOf(
27 | "NEW_USER_BONUS_PET",
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/controller/response/RankHistoryResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.controller.response
2 |
3 | import org.gitanimals.rank.domain.RankQueryRepository
4 | import org.gitanimals.rank.domain.history.RankHistory
5 |
6 | data class RankHistoryResponse(
7 | val winner: List
8 | ) {
9 | data class Result(
10 | val id: String,
11 | val name: String,
12 | val rank: Int,
13 | val prize: Int,
14 | val rankType: RankQueryRepository.RankType,
15 | )
16 |
17 | companion object {
18 | fun from(rankHistories: List): RankHistoryResponse {
19 | return RankHistoryResponse(
20 | winner = rankHistories.map {
21 | Result(
22 | id = it.winner.id.toString(),
23 | name = it.winner.name,
24 | rank = it.ranks,
25 | prize = it.prize,
26 | rankType = it.rankType,
27 | )
28 | }.sortedBy { it.rank }
29 | )
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | ### Project ###
4 | group=org.gitanimals
5 | version=2.5.1
6 |
7 | ### Spring ###
8 | springDependencyManagementVersion=1.1.1
9 | springbootVersion=3.2.1
10 | springMockkVersion=4.0.0
11 |
12 | ### Jetbrain ###
13 | jetbrainKotlinVersion=1.9.22
14 | jvmTarget=21
15 |
16 | ### Database ###
17 | mysqlConnectorVersion=8.0.33
18 |
19 | ### Kotest ###
20 | kotestVersion=5.7.2
21 | kotestExtensionSpringVersion=1.1.3
22 |
23 | ### TestContainer ###
24 | testContainerVersion=1.19.3
25 |
26 | ### Netx ###
27 | netxVersion=0.5.0
28 |
29 | ### Sentry ###
30 | sentryVersion=4.4.0
31 |
32 | ### RestAssured ###
33 | restAssuredVersion=5.4.0
34 |
35 | ### H2version ###
36 | h2Version=1.4.200
37 |
38 | ### Snowflake ###
39 | snowflakeVersion=5.2.5
40 |
41 | ### Caffeine cache ###
42 | caffeineCacheVersion=3.1.8
43 |
44 | ### Ecs logback encoder ###
45 | logbackEcsEncoderVersion=1.6.0
46 |
47 | ### Slack ###
48 | slackVersion=1.40.2
49 |
50 | ### Jackson version ###
51 | jacksonVersion=2.15.3
52 |
53 | ### Redisson ###
54 | redissonVersion=3.47.0
55 |
56 | ### Slf4jKotinx ###
57 | slf4jKotlinxVersion = 1.9.0
58 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/response/PersonaResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain.response
2 |
3 | import org.gitanimals.core.PersonaGrade
4 | import org.gitanimals.render.domain.Persona
5 | import org.gitanimals.core.PersonaType
6 |
7 | data class PersonaResponse(
8 | val id: String,
9 | val type: PersonaType,
10 | val level: String,
11 | val visible: Boolean,
12 | val appVisible: Boolean,
13 | val dropRate: String,
14 | val grade: PersonaGrade,
15 | val isEvolutionable: Boolean,
16 | ) {
17 | companion object {
18 | fun from(persona: Persona): PersonaResponse {
19 | return PersonaResponse(
20 | id = persona.id.toString(),
21 | type = persona.getType(),
22 | level = persona.level.value.toString(),
23 | visible = persona.visible,
24 | appVisible = persona.appVisible,
25 | dropRate = persona.getType().getDropRate(),
26 | grade = persona.getType().grade,
27 | isEvolutionable = persona.isEvolutionable(),
28 | )
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.profiles.active=test
2 | spring.datasource.driver-class-name = org.h2.Driver
3 | spring.datasource.url = jdbc:h2:mem:test;MODE=MySQL;DATABASE_TO_LOWER=TRUE
4 |
5 | spring.jpa.hibernated.ddl-auto = create-drop
6 | spring.jpa.database-platform = org.hibernate.dialect.H2Dialect
7 |
8 | spring.datasource.hikari.maximum-pool-size = 4
9 | spring.datasource.hikari.pool-name = H2_TEST_POOL
10 |
11 | ### FOR DEBUGGING ###
12 | logging.level.org.hibernate.SQL = debug
13 | logging.level.org.hibernate.type.descriptor.sql = trace
14 |
15 | spring.jpa.properties.hibernate.format_sql = true
16 | spring.jpa.properties.hibernate.highlight_sql = true
17 | spring.jpa.properties.hibernate.use_sql_comments = true
18 |
19 | ### NETX ###
20 | netx.mode=redis
21 | netx.host=localhost
22 | netx.port=6379
23 | netx.group=render
24 | netx.node-id=1
25 | netx.node-name=render-1
26 | netx.recovery-milli=1000
27 | netx.orphan-milli=60000
28 | netx.backpressure=40
29 | netx.logging.level=info
30 |
31 | ### GITHUB ###
32 | github.token=1234
33 |
34 | internal.secret=foo
35 | internal.auth.secret=p4K86j8sl9LDufoN/2rpj8df+gc2SAGHUKKup6l695o=
36 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/deadletter/DeadLetterRedisMessageListenerConfiguration.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.deadletter
2 |
3 | import org.gitanimals.core.redis.RedisPubSubChannel
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.data.redis.connection.RedisConnectionFactory
7 | import org.springframework.data.redis.listener.ChannelTopic
8 | import org.springframework.data.redis.listener.RedisMessageListenerContainer
9 |
10 | @Configuration
11 | class DeadLetterRedisMessageListenerConfiguration(
12 | private val redisConnectionFactory: RedisConnectionFactory,
13 | private val deadLetterRelayEventListener: DeadLetterRelayEventListener,
14 | ) {
15 |
16 | @Bean
17 | fun deadLetterListenerContainer(): RedisMessageListenerContainer {
18 | return RedisMessageListenerContainer().apply {
19 | this.connectionFactory = redisConnectionFactory
20 | this.addMessageListener(
21 | deadLetterRelayEventListener,
22 | ChannelTopic(RedisPubSubChannel.SLACK_INTERACTED)
23 | )
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/infra/GuildRedisEventSubscriber.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.infra
2 |
3 | import org.gitanimals.core.redis.RedisPubSubChannel
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.data.redis.connection.RedisConnectionFactory
7 | import org.springframework.data.redis.listener.ChannelTopic
8 | import org.springframework.data.redis.listener.RedisMessageListenerContainer
9 |
10 | @Configuration
11 | class GuildRedisEventSubscriber(
12 | private val redisConnectionFactory: RedisConnectionFactory,
13 | private val guildUpdateGuildContributionMessageListener: GuildUpdateGuildContributionMessageListener,
14 | ) {
15 |
16 | @Bean
17 | fun guildRedisListenerContainer(): RedisMessageListenerContainer {
18 | return RedisMessageListenerContainer().apply {
19 | this.connectionFactory = redisConnectionFactory
20 | this.addMessageListener(
21 | guildUpdateGuildContributionMessageListener,
22 | ChannelTopic(RedisPubSubChannel.USER_CONTRIBUTION_UPDATED),
23 | )
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/render/domain/UserFixture.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import org.gitanimals.core.IdGenerator
4 | import org.gitanimals.core.PersonaType
5 | import org.gitanimals.render.domain.value.Contribution
6 | import org.gitanimals.render.domain.value.Level
7 |
8 | fun user(
9 | id: Long = IdGenerator.generate(),
10 | name: String = "devxb",
11 | personas: MutableList = mutableListOf(),
12 | contributions: MutableList = mutableListOf(),
13 | authInfo: UserAuthInfo? = UserAuthInfo(EntryPoint.GITHUB, id.toString()),
14 | visit: Long = 0L,
15 | ): User {
16 | return User(
17 | id = id,
18 | name = name,
19 | personas = personas,
20 | contributions = contributions,
21 | visit = visit,
22 | version = 0L,
23 | lastPersonaGivePoint = 0,
24 | authInfo = authInfo,
25 | )
26 | }
27 |
28 | fun persona(
29 | id: Long = IdGenerator.generate(),
30 | type: PersonaType = PersonaType.CAT,
31 | user: User,
32 | ) = Persona(
33 | id = id,
34 | type = type,
35 | level = Level(0),
36 | visible = false,
37 | appVisible = false,
38 | user = user,
39 | )
40 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/filter/CorsFilter.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.filter
2 |
3 | import jakarta.servlet.Filter
4 | import jakarta.servlet.FilterChain
5 | import jakarta.servlet.ServletRequest
6 | import jakarta.servlet.ServletResponse
7 | import jakarta.servlet.http.HttpServletResponse
8 | import org.springframework.http.HttpHeaders
9 | import org.springframework.stereotype.Component
10 |
11 | @Component
12 | class CorsFilter : Filter {
13 |
14 | override fun doFilter(
15 | request: ServletRequest,
16 | response: ServletResponse,
17 | chain: FilterChain
18 | ) {
19 | (response as HttpServletResponse).allowCors()
20 | chain.doFilter(request, response)
21 | }
22 |
23 | private fun HttpServletResponse.allowCors(): ServletResponse {
24 | this.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*")
25 | this.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "*")
26 | this.addHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600")
27 | this.addHeader(
28 | HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
29 | "Origin, X-Requested-With, Content-Type, Accept, Authorization, Api-Version, Image-Secret"
30 | )
31 | return this
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/UserContributionRank.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Entity
5 | import jakarta.persistence.Table
6 | import org.gitanimals.core.IdGenerator
7 |
8 | @Entity
9 | @Table(name = "user_contribution_rank")
10 | class UserContributionRank(
11 | id: Long,
12 | image: String,
13 |
14 | @Column(name = "user_id", nullable = false, unique = true)
15 | val userId: Long,
16 | @Column(name = "username", nullable = false, unique = true)
17 | var username: String,
18 | @Column(name = "weekly_contributions", nullable = false)
19 | var weeklyContributions: Long,
20 | ) : Rank(id, image) {
21 |
22 | companion object {
23 |
24 | fun create(
25 | image: String,
26 | userId: Long,
27 | username: String,
28 | weeklyContributions: Long,
29 | ): UserContributionRank {
30 | return UserContributionRank(
31 | id = IdGenerator.generate(),
32 | image = image,
33 | userId = userId,
34 | username = username,
35 | weeklyContributions = weeklyContributions,
36 | )
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/IdentityApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app
2 |
3 | import org.springframework.http.HttpHeaders
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.bind.annotation.RequestHeader
6 | import org.springframework.web.bind.annotation.RequestParam
7 | import org.springframework.web.service.annotation.GetExchange
8 | import org.springframework.web.service.annotation.PostExchange
9 |
10 | interface IdentityApi {
11 |
12 | @GetExchange("/users")
13 | fun getUserByToken(@RequestHeader(HttpHeaders.AUTHORIZATION) token: String): UserResponse
14 |
15 | @PostExchange("/internals/users/points/increases/by-username/{username}")
16 | fun increaseUserPointsByUsername(
17 | @PathVariable("username") username: String,
18 | @RequestParam("point") point: Int,
19 | @RequestParam("idempotency-key") idempotencyKey: String,
20 | )
21 |
22 | @GetExchange("/internals/users/{user-id}")
23 | fun getUserById(
24 | @PathVariable("user-id") userId: Long
25 | ): UserResponse
26 |
27 | data class UserResponse(
28 | val id: String,
29 | val username: String,
30 | val points: String,
31 | val profileImage: String,
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/GuildContributionRank.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain
2 |
3 | import jakarta.persistence.Column
4 | import jakarta.persistence.Entity
5 | import jakarta.persistence.Table
6 | import org.gitanimals.core.IdGenerator
7 |
8 | @Entity
9 | @Table(name = "guild_contribution_rank")
10 | class GuildContributionRank(
11 | id: Long,
12 | image: String,
13 |
14 | @Column(name = "guild_id", nullable = false, unique = true)
15 | val guildId: Long,
16 | @Column(name = "guild_name", nullable = false, unique = true)
17 | val guildName: String,
18 | @Column(name = "weekly_contributions", nullable = false)
19 | var weeklyContributions: Long,
20 | ) : Rank(id, image) {
21 |
22 | companion object {
23 | fun create(
24 | image: String,
25 | guildId: Long,
26 | guildName: String,
27 | weeklyContributions: Long,
28 | ): GuildContributionRank {
29 | return GuildContributionRank(
30 | id = IdGenerator.generate(),
31 | image = image,
32 | guildId = guildId,
33 | guildName = guildName,
34 | weeklyContributions = weeklyContributions,
35 | )
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/auth/RequiredUserEntryPointAspect.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.auth
2 |
3 | import org.aspectj.lang.ProceedingJoinPoint
4 | import org.aspectj.lang.annotation.Around
5 | import org.aspectj.lang.annotation.Aspect
6 | import org.gitanimals.core.auth.UserEntryPointValidationExtension.withUserEntryPointValidation
7 | import org.slf4j.LoggerFactory
8 | import org.springframework.stereotype.Component
9 |
10 | @Aspect
11 | @Component
12 | class RequiredUserEntryPointAspect {
13 |
14 | private val logger = LoggerFactory.getLogger(this::class.simpleName)
15 |
16 | @Around("@annotation(requiredUserEntryPoints)")
17 | fun validate(
18 | proceedingJoinPoint: ProceedingJoinPoint,
19 | requiredUserEntryPoints: RequiredUserEntryPoints,
20 | ): Any? {
21 | return withUserEntryPointValidation(
22 | expectedUserEntryPoints = requiredUserEntryPoints.expected,
23 | onSuccess = { proceedingJoinPoint.proceed() },
24 | failMessage = {
25 | val message =
26 | "ExpectedUserEntryPoints: \"${requiredUserEntryPoints.expected}\" but request user entryPoint is \"$it\""
27 | logger.info("[RequiredUserEntryPointAspect] $message")
28 | message
29 | }
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/app/GuildApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.app
2 |
3 | import com.fasterxml.jackson.annotation.JsonFormat
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.service.annotation.GetExchange
6 | import java.time.Instant
7 |
8 | interface GuildApi {
9 |
10 | @GetExchange("/internals/guilds/by-title/{title}")
11 | fun getGuildByTitle(@PathVariable("title") title: String): GuildResponse
12 |
13 | @GetExchange("/guilds/{guildId}")
14 | fun getGuildById(@PathVariable("guildId") guildId: Long): GuildResponse
15 |
16 | data class GuildResponse(
17 | val id: String,
18 | val title: String,
19 | val body: String,
20 | val guildIcon: String,
21 | val leader: org.gitanimals.guild.app.response.GuildResponse.Leader,
22 | val farmType: String,
23 | val totalContributions: String,
24 | val members: List,
25 | val waitMembers: List,
26 | val autoJoin: Boolean,
27 | @JsonFormat(
28 | shape = JsonFormat.Shape.STRING,
29 | pattern = "yyyy-MM-dd HH:mm:ss",
30 | timezone = "UTC"
31 | )
32 | val createdAt: Instant,
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/deploy/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM arm64v8/openjdk:21
2 |
3 | ARG DB_URL
4 | ARG DB_USERNAME
5 | ARG DB_PASSWORD
6 | ARG GITHUB_TOKEN
7 | ARG REDIS_HOST
8 | ARG REDIS_PORT
9 | ARG INTERNAL_SECRET
10 | ARG SLACK_TOKEN
11 | ARG RELAY_APPROVE_TOKEN
12 | ARG INTERNAL_AUTH_SECRET
13 | ARG INTERNAL_IMAGE_SECRET
14 |
15 | ARG JAR_FILE=./*.jar
16 | COPY ${JAR_FILE} gitanimals-render.jar
17 |
18 | ENV db_url=${DB_URL} \
19 | db_username=${DB_USERNAME} \
20 | db_password=${DB_PASSWORD} \
21 | github_token=${GITHUB_TOKEN} \
22 | redis_host=${REDIS_HOST} \
23 | redis_port=${REDIS_PORT} \
24 | internal_secret=${INTERNAL_SECRET} \
25 | slack_token=${SLACK_TOKEN} \
26 | relay_approve_token=${RELAY_APPROVE_TOKEN} \
27 | internal_auth_secret=${INTERNAL_AUTH_SECRET} \
28 | internal_image_secret=${INTERNAL_IMAGE_SECRET}
29 |
30 | ENTRYPOINT java -jar gitanimals-render.jar \
31 | --spring.datasource.url=${db_url} \
32 | --spring.datasource.username=${db_username} \
33 | --spring.datasource.password=${db_password} \
34 | --netx.host=${redis_host} \
35 | --netx.port=${redis_port} \
36 | --github.token=${github_token} \
37 | --internal.secret=${internal_secret} \
38 | --slack.token=${slack_token} \
39 | --relay.approve.token=${relay_approve_token} \
40 | --internal.auth.secret=${internal_auth_secret} \
41 | --internal.image.secret=${internal_image_secret}
42 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/core/lock/DistributedLockTest.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.lock
2 |
3 | import com.ninjasquad.springmockk.MockkBean
4 | import io.kotest.core.annotation.DisplayName
5 | import io.kotest.core.spec.style.StringSpec
6 | import io.kotest.matchers.shouldBe
7 | import io.mockk.every
8 | import io.mockk.mockk
9 | import org.redisson.api.RLock
10 | import org.redisson.api.RedissonClient
11 | import org.springframework.test.context.ContextConfiguration
12 |
13 | @ContextConfiguration(
14 | classes = [
15 | RedisDistributedLockService::class
16 | ]
17 | )
18 | @DisplayName("DistributedLock 클래스의")
19 | class DistributedLockTest(
20 | private val redisDistributedLockService: RedisDistributedLockService,
21 | @MockkBean(relaxed = true) private val redissonClient: RedissonClient,
22 | ) : StringSpec({
23 |
24 | "Redis 구현채는 Lock획득에 실패했을때, whenAcquireFail 구문을 실행한다"() {
25 | // given
26 | val rLock = mockk()
27 | every { redissonClient.getLock(any()) } returns rLock
28 | every { rLock.tryLock(any(), any(), any()) } returns false
29 |
30 | // when
31 | val result = redisDistributedLockService.withLock(
32 | key = "test",
33 | whenAcquireFail = { "whenAcquireFail" },
34 | ) { "success" }
35 |
36 | // then
37 | result shouldBe "whenAcquireFail"
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/app/GetRankByUsernameFacade.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.app
2 |
3 | import org.gitanimals.rank.domain.RankQueryRepository
4 | import org.gitanimals.rank.domain.UserContributionRankService
5 | import org.gitanimals.rank.domain.response.RankResponse
6 | import org.springframework.stereotype.Component
7 |
8 | @Component
9 | class GetRankByUsernameFacade(
10 | private val userContributionRankService: UserContributionRankService,
11 | private val rankQueryRepository: RankQueryRepository,
12 | ) {
13 |
14 | fun invoke(username: String): RankResponse {
15 | val userContributionRank = userContributionRankService.findUserRankByUsername(username)
16 |
17 | checkNotNull(userContributionRank) {
18 | "UserContributionRank is null username: $username"
19 | }
20 |
21 | val rankQueryResponse = rankQueryRepository.getRankByRankId(
22 | rankType = RankQueryRepository.RankType.WEEKLY_USER_CONTRIBUTIONS,
23 | rankId = userContributionRank.id,
24 | )
25 |
26 | return RankResponse(
27 | id = userContributionRank.userId.toString(),
28 | rank = rankQueryResponse.rank + 1,
29 | image = userContributionRank.image,
30 | name = userContributionRank.username,
31 | contributions = userContributionRank.weeklyContributions,
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/JacksonConfig.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import com.fasterxml.jackson.annotation.JsonAutoDetect
4 | import com.fasterxml.jackson.annotation.JsonCreator
5 | import com.fasterxml.jackson.annotation.PropertyAccessor
6 | import com.fasterxml.jackson.databind.DeserializationFeature
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
9 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
10 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
11 | import org.springframework.context.annotation.Bean
12 | import org.springframework.context.annotation.Configuration
13 | import org.springframework.context.annotation.Primary
14 |
15 | @Configuration
16 | class JacksonConfig {
17 |
18 | @Bean
19 | @Primary
20 | fun objectMapper(): ObjectMapper {
21 | return jacksonObjectMapper()
22 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
23 | .registerModule(ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
24 | .registerModule(JavaTimeModule())
25 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
26 | .configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false)
27 | .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/app/UserStatisticSchedule.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.app
2 |
3 | import org.gitanimals.core.instant
4 | import org.gitanimals.core.toKr
5 | import org.gitanimals.render.domain.UserStatisticService
6 | import org.gitanimals.render.domain.event.UserYesterdayReport
7 | import org.rooftop.netx.api.SagaManager
8 | import org.springframework.scheduling.annotation.Scheduled
9 | import org.springframework.stereotype.Service
10 |
11 | @Service
12 | class UserStatisticSchedule(
13 | private val sagaManager: SagaManager,
14 | private val userStatisticService: UserStatisticService,
15 | ) {
16 |
17 | @Scheduled(cron = EVERY_9AM, zone = "Asia/Seoul")
18 | fun sendYesterdayNewUserReport() {
19 | val yesterday = instant().toKr().minusDays(1)
20 | val yesterdayUserCount = userStatisticService.getYesterdayUserCount()
21 | val totalUserCount = userStatisticService.getTotalUserCount()
22 |
23 | val userYesterdayReport = UserYesterdayReport(
24 | date = yesterday,
25 | yesterdayNewUserCount = yesterdayUserCount,
26 | totalUserCount = totalUserCount,
27 | serverName = SERVER_NAME,
28 | )
29 |
30 | sagaManager.startSync(userYesterdayReport)
31 | }
32 |
33 | private companion object {
34 | private const val EVERY_9AM = "0 0 9 * * ?"
35 | private const val SERVER_NAME = "RENDER"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/response/GuildPagingResponse.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app.response
2 |
3 | import org.gitanimals.guild.domain.Guild
4 | import org.springframework.data.domain.Page
5 |
6 | data class GuildPagingResponse(
7 | val guilds: List,
8 | val pagination: Pagination,
9 | ) {
10 |
11 | data class Pagination(
12 | val totalRecords: Int,
13 | val currentPage: Int,
14 | val totalPages: Int,
15 | val nextPage: Int?,
16 | val prevPage: Int?,
17 | )
18 |
19 | companion object {
20 |
21 | fun from(guilds: Page, forceCurrentPage: Int): GuildPagingResponse {
22 | return GuildPagingResponse(
23 | guilds = guilds.map { GuildResponse.from(it) }.toList(),
24 | pagination = Pagination(
25 | totalRecords = guilds.count(),
26 | currentPage = forceCurrentPage,
27 | totalPages = guilds.totalPages,
28 | nextPage = when (guilds.hasNext()) {
29 | true -> guilds.number + 1
30 | false -> null
31 | },
32 | prevPage = when (guilds.hasPrevious()) {
33 | true -> guilds.number - 1
34 | false -> null
35 | },
36 | )
37 | )
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/render/domain/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.render.domain
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository
4 | import org.springframework.data.jpa.repository.Query
5 | import org.springframework.data.repository.query.Param
6 |
7 | interface UserRepository : JpaRepository {
8 |
9 | fun findByName(name: String): User?
10 |
11 | @Query(
12 | """
13 | select u from user as u
14 | left join fetch u.contributions
15 | where u.name = :name
16 | """
17 | )
18 | fun findByNameWithContributions(@Param("name") name: String): User?
19 |
20 | @Query(
21 | """
22 | select u from user as u
23 | left join fetch u.contributions
24 | where u.name in :usernames
25 | """
26 | )
27 | fun findAllByIdsWithContributions(@Param("usernames") usernames: Set): List
28 |
29 | @Query(
30 | """
31 | select u from user as u
32 | where u.authInfo.entryPoint = :entryPoint
33 | and u.authInfo.authenticationId = :authenticationId
34 | """
35 | )
36 | fun findByEntryPointAndAuthenticationId(
37 | @Param("entryPoint") entryPoint: EntryPoint,
38 | @Param("authenticationId") authenticationId: String,
39 | ): User?
40 |
41 | fun existsByName(name: String): Boolean
42 |
43 | fun deleteByName(name: String)
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.app
2 |
3 | import org.gitanimals.core.PersonaType
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.bind.annotation.RequestBody
6 | import org.springframework.web.bind.annotation.RequestHeader
7 | import org.springframework.web.service.annotation.GetExchange
8 |
9 | interface RenderApi {
10 |
11 | @GetExchange("/users/{username}")
12 | fun getUserByName(@PathVariable("username") username: String): UserResponse
13 |
14 | @GetExchange("/internals/personas/all")
15 | fun getAllPersonasByUserIdsAndPersonaIds(
16 | @RequestHeader(INTERNAL_SECRET_KEY) internalSecret: String,
17 | @RequestBody usernameAndPersonaIdRequests: List,
18 | ): List
19 |
20 | data class UserResponse(
21 | val id: String,
22 | val name: String,
23 | val totalContributions: String,
24 | val personas: List,
25 | ) {
26 |
27 | data class PersonaResponse(
28 | val id: String,
29 | val level: String,
30 | val type: PersonaType,
31 | )
32 | }
33 |
34 | data class UsernameAndPersonaIdRequest(
35 | val username: String,
36 | val personaId: Long,
37 | )
38 |
39 | private companion object {
40 | private const val INTERNAL_SECRET_KEY = "Internal-Secret"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/domain/history/RankHistoryService.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.domain.history
2 |
3 | import org.gitanimals.core.IdGenerator
4 | import org.gitanimals.rank.domain.RankQueryRepository.RankType
5 | import org.gitanimals.rank.domain.history.request.InitRankHistoryRequest
6 | import org.springframework.cache.annotation.Cacheable
7 | import org.springframework.stereotype.Service
8 | import org.springframework.transaction.annotation.Transactional
9 |
10 | @Service
11 | class RankHistoryService(
12 | private val rankHistoryRepository: RankHistoryRepository,
13 | ) {
14 |
15 | @Transactional
16 | fun initTop3Rank(
17 | initRankHistoryRequests: List,
18 | ) {
19 | rankHistoryRepository.saveAll(initRankHistoryRequests.map {
20 | RankHistory(
21 | id = IdGenerator.generate(),
22 | ranks = it.rank,
23 | prize = it.prize,
24 | rankType = it.rankType,
25 | winner = Winner(
26 | id = it.winnerId,
27 | name = it.winnerName,
28 | )
29 | )
30 | })
31 | }
32 |
33 | @Cacheable(cacheNames = ["find_top3_history_by_rank_type"])
34 | fun findTop3HistoryByRankType(rankType: RankType): List {
35 | return rankHistoryRepository.findTop3ByRankTypeOrderByCreatedAtDesc(rankType)
36 | .sortedBy { it.ranks }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/rank/infra/listener/RankRedisEventSubscriber.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.rank.infra.listener
2 |
3 | import org.gitanimals.core.redis.RedisPubSubChannel
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.data.redis.connection.RedisConnectionFactory
7 | import org.springframework.data.redis.listener.ChannelTopic
8 | import org.springframework.data.redis.listener.RedisMessageListenerContainer
9 |
10 | @Configuration
11 | class RankRedisEventSubscriber(
12 | private val redisConnectionFactory: RedisConnectionFactory,
13 | private val rankUpdateGuildContributionMessageListener: RankUpdateGuildContributionMessageListener,
14 | private val updateUserContributionMessageListener: UpdateUserContributionMessageListener,
15 | ) {
16 |
17 | @Bean
18 | fun rankRedisListenerContainer(): RedisMessageListenerContainer {
19 | return RedisMessageListenerContainer().apply {
20 | this.connectionFactory = redisConnectionFactory
21 | this.addMessageListener(
22 | updateUserContributionMessageListener,
23 | ChannelTopic(RedisPubSubChannel.USER_CONTRIBUTION_UPDATED),
24 | )
25 | this.addMessageListener(
26 | rankUpdateGuildContributionMessageListener,
27 | ChannelTopic(RedisPubSubChannel.GUILD_CONTRIBUTION_UPDATED),
28 | )
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/appender/SlackAppender.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.appender
2 |
3 | import ch.qos.logback.classic.Level
4 | import ch.qos.logback.classic.spi.ILoggingEvent
5 | import ch.qos.logback.core.AppenderBase
6 | import com.slack.api.Slack
7 | import com.slack.api.methods.MethodsClient
8 | import com.slack.api.methods.request.chat.ChatPostMessageRequest
9 |
10 | class SlackAppender : AppenderBase() {
11 |
12 | private lateinit var slack: MethodsClient
13 |
14 | private val errorChannel = "C080GR85WM9"
15 | private val warnChannel = "C08977RL38C"
16 |
17 | override fun append(eventObject: ILoggingEvent) {
18 | runCatching {
19 | val channel = when (eventObject.level) {
20 | Level.WARN -> warnChannel
21 | Level.ERROR -> errorChannel
22 | else -> NOT_LOGGING
23 | }
24 |
25 | if (channel == NOT_LOGGING) {
26 | return
27 | }
28 |
29 | val request: ChatPostMessageRequest = ChatPostMessageRequest.builder()
30 | .channel(channel)
31 | .text(eventObject.formattedMessage)
32 | .build()
33 | slack.chatPostMessage(request)
34 | }
35 | }
36 |
37 | fun setToken(token: String) {
38 | this.slack = Slack.getInstance().methods(token)
39 | }
40 |
41 | companion object {
42 | private const val NOT_LOGGING = "NOT LOGGING"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.guild.saga
2 |
3 | import org.gitanimals.guild.app.RenderApi
4 | import org.gitanimals.guild.domain.GuildService
5 | import org.gitanimals.guild.saga.event.PersonaDeleted
6 | import org.rooftop.netx.api.SagaStartEvent
7 | import org.rooftop.netx.api.SagaStartListener
8 | import org.rooftop.netx.api.SuccessWith
9 | import org.rooftop.netx.meta.SagaHandler
10 |
11 | @SagaHandler
12 | class PersonaDeletedSagaHandler(
13 | private val guildService: GuildService,
14 | private val renderApi: RenderApi,
15 | ) {
16 |
17 | @SagaStartListener(
18 | event = PersonaDeleted::class,
19 | noRollbackFor = [IllegalStateException::class],
20 | successWith = SuccessWith.END,
21 | )
22 | fun handlePersonaDeletedEvent(sagaStartEvent: SagaStartEvent) {
23 | val personaDeleted = sagaStartEvent.decodeEvent(PersonaDeleted::class)
24 |
25 | val changePersona =
26 | renderApi.getUserByName(personaDeleted.username).personas.maxByOrNull { it.level }
27 | ?: throw IllegalStateException("Cannot find any persona by username \"${personaDeleted.username}\"")
28 |
29 | guildService.deletePersonaSync(
30 | userId = personaDeleted.userId,
31 | deletedPersonaId = personaDeleted.personaId,
32 | changePersonaId = changePersona.id.toLong(),
33 | changePersonaType = changePersona.type,
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.profiles.active=prod
2 |
3 | server.name=gitanimals-render
4 | server.port=8080
5 |
6 | spring.datasource.url=jdbc:mysql://localhost:3306/gitanimalsrender
7 | spring.datasource.username=root
8 | spring.datasource.password=0000
9 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
10 |
11 | spring.jpa.hibernate.ddl-auto=update
12 | spring.jpa.open-in-view=false
13 |
14 | netx.mode=redis
15 | netx.host=localhost
16 | netx.port=6379
17 | netx.group=render
18 | netx.node-id=1
19 | netx.node-name=render-1
20 | netx.recovery-milli=1000
21 | netx.orphan-milli=60000
22 | netx.backpressure=40
23 | netx.logging.level=info
24 |
25 | slack.token=xoxb-
26 | github.token=a
27 |
28 | sentry.dsn=https://fe1aaf784ec135343909a4a0dfe4f0eb@o4505051656486912.ingest.us.sentry.io/4507088960684032
29 | sentry.traces-sample-rate=1.0
30 |
31 | internal.secret=a
32 | internal.auth.secret=b
33 | internal.image.secret=c
34 |
35 | spring.application.name=render.gitanimals
36 | management.endpoints.web.exposure.include=health,prometheus
37 | management.metrics.tags.application=${spring.application.name}
38 | management.endpoint.health.show-details=never
39 | management.health.defaults.enabled=false
40 | management.health.ping.enabled=true
41 |
42 | logging.level.root=INFO
43 |
44 | logging.level.org.springframework.web.servlet.PageNotFound=ERROR
45 | spring.mvc.throw-exception-if-no-handler-found=true
46 | spring.web.resources.add-mappings=false
47 |
48 | relay.approve.token=foo
49 |
--------------------------------------------------------------------------------
/src/test/kotlin/org/gitanimals/core/CalculatePersonaDropRate.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import io.kotest.core.spec.style.StringSpec
4 |
5 | internal class CalculatePersonaDropRate : StringSpec({
6 |
7 | "특정 진화 펫 등장확률 테스트" {
8 | val dropCount = 10000
9 | val expectedPersona = PersonaType.PIG_ROBOT
10 |
11 | val results = mutableMapOf()
12 | repeat(dropCount) {
13 | val evolutionResult = expectedPersona.randomEvolution()
14 |
15 | results[evolutionResult] = (results[evolutionResult]?.plus(1)) ?: 1
16 | }
17 |
18 | println("--- 모수: $dropCount ---")
19 | results.forEach { (key, value) ->
20 | val dropPercentage = (value.toDouble() / dropCount.toDouble()) * 100
21 | println("$key -> $dropPercentage% ($value)")
22 | }
23 | }
24 |
25 | "특정 펫 등장 테스트" {
26 | val dropCount = 10000
27 | val expectedPersona = PersonaType.HAMSTER_SNOW
28 |
29 | val results = mutableMapOf()
30 | repeat(dropCount) {
31 | val dropResult: PersonaType = PersonaType.random()
32 |
33 | results[dropResult] = (results[dropResult]?.plus(1)) ?: 1
34 | }
35 |
36 | println("--- 모수: $dropCount ---")
37 | results.forEach { (key, value) ->
38 | val dropPercentage = (value.toDouble() / dropCount.toDouble()) * 100
39 | println("$key -> $dropPercentage% ($value)")
40 | }
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/redis/RedisPubSubEventListener.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.redis
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.beans.factory.annotation.Qualifier
6 | import org.springframework.context.event.EventListener
7 | import org.springframework.data.redis.core.StringRedisTemplate
8 | import org.springframework.scheduling.annotation.Async
9 | import org.springframework.stereotype.Component
10 |
11 | @Component
12 | class RedisPubSubEventListener(
13 | private val objectMapper: ObjectMapper,
14 | @Qualifier("gitanimalsRedisTemplate") private val redisTemplate: StringRedisTemplate,
15 | ) {
16 |
17 | private val logger = LoggerFactory.getLogger(this::class.simpleName)
18 |
19 | @Async
20 | @EventListener(value = [AsyncRedisPubSubEvent::class])
21 | fun handleAsyncRedisPubSubEvent(event: AsyncRedisPubSubEvent) {
22 | runCatching {
23 | val eventBody = objectMapper.writeValueAsString(event)
24 | redisTemplate.convertAndSend(
25 | event.channel,
26 | eventBody,
27 | )
28 | eventBody
29 | }.onSuccess {
30 | logger.info("Publish event: \"$it\" to channel: \"${event.channel}\"")
31 | }.onFailure {
32 | logger.error(
33 | "Cannot publish event to redis. event: $event, channel: ${event.channel}",
34 | it
35 | )
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/redis/RedisConfiguration.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.redis
2 |
3 | import org.springframework.beans.factory.annotation.Value
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.data.redis.connection.RedisConnectionFactory
7 | import org.springframework.data.redis.connection.RedisPassword
8 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration
9 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
10 | import org.springframework.data.redis.core.StringRedisTemplate
11 |
12 | @Configuration
13 | class RedisConfiguration(
14 | @Value("\${netx.host}") private val host: String,
15 | @Value("\${netx.port}") private val port: String,
16 | @Value("\${netx.password:0000}") private val password: String,
17 | ) {
18 |
19 | @Bean
20 | fun gitanimalsRedisTemplate(): StringRedisTemplate =
21 | StringRedisTemplate(redisConnectionFactory())
22 |
23 | @Bean
24 | fun redisConnectionFactory(): RedisConnectionFactory {
25 | val port: String = System.getProperty("netx.port") ?: port
26 |
27 | val redisStandaloneConfiguration = RedisStandaloneConfiguration()
28 | redisStandaloneConfiguration.hostName = host
29 | redisStandaloneConfiguration.port = port.toInt()
30 | redisStandaloneConfiguration.password = RedisPassword.of(password)
31 |
32 | return LettuceConnectionFactory(redisStandaloneConfiguration)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/auth/UserEntryPointValidationExtension.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.auth
2 |
3 | import org.springframework.stereotype.Component
4 |
5 | object UserEntryPointValidationExtension {
6 |
7 | private lateinit var internalAuth: InternalAuth
8 |
9 | fun withUserEntryPointValidation(
10 | expectedUserEntryPoints: Array,
11 | onSuccess: () -> T,
12 | failMessage: (UserEntryPoint?) -> String = { userEntryPoint ->
13 | "ExpectedUserEntryPoints: \"$expectedUserEntryPoints\" but request user entryPoint is \"$userEntryPoint\""
14 | }
15 | ): T {
16 | val userEntryPoint = runCatching {
17 | val userEntryPointString = internalAuth.getUserEntryPoint {
18 | throw IllegalArgumentException(failMessage.invoke(null))
19 | }
20 | UserEntryPoint.valueOf(userEntryPointString)
21 | }.getOrElse {
22 | throw IllegalArgumentException(failMessage.invoke(null))
23 | }
24 |
25 | require(
26 | expectedUserEntryPoints.contains(UserEntryPoint.ANY)
27 | || userEntryPoint in expectedUserEntryPoints
28 | ) { failMessage.invoke(userEntryPoint) }
29 |
30 | return onSuccess.invoke()
31 | }
32 |
33 | @Component
34 | class UserEntryPointValidationExtensionBeanInjector(
35 | internalAuth: InternalAuth,
36 | ) {
37 |
38 | init {
39 | UserEntryPointValidationExtension.internalAuth = internalAuth
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/supports/deadletter/DeadLetterEventPublisher.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.supports.deadletter
2 |
3 | import org.gitanimals.core.IdGenerator
4 | import org.gitanimals.core.filter.MDCFilter.Companion.TRACE_ID
5 | import org.rooftop.netx.api.SagaEvent
6 | import org.rooftop.netx.api.TypeReference
7 | import org.rooftop.netx.spi.DeadLetterListener
8 | import org.slf4j.LoggerFactory
9 | import org.slf4j.MDC
10 | import org.springframework.context.ApplicationEventPublisher
11 |
12 | class DeadLetterEventPublisher(
13 | private val applicationEventPublisher: ApplicationEventPublisher,
14 | ) : DeadLetterListener {
15 |
16 | private val logger = LoggerFactory.getLogger(this::class.simpleName)
17 |
18 | override fun listen(deadLetterId: String, sagaEvent: SagaEvent) {
19 | runCatching {
20 | MDC.put(TRACE_ID, IdGenerator.generate().toString())
21 | applicationEventPublisher.publishEvent(
22 | DeadLetterEvent(
23 | deadLetterId = deadLetterId,
24 | sagaId = sagaEvent.id,
25 | group = sagaEvent.group,
26 | nodeName = sagaEvent.nodeName,
27 | deadLetter = sagaEvent.decodeEvent(object: TypeReference