├── .github
├── CODEOWNERS
└── workflows
│ └── deploy.yml
├── .gitignore
├── .idea
└── checkstyle-idea.xml
├── LICENSE.md
├── README-en.md
├── README-ja.md
├── README-zhcn.md
├── README.md
├── build.gradle
├── deploy
├── api
│ └── Dockerfile
└── filebeat
│ ├── Dockerfile
│ └── filebeat.yml
├── docs
├── bbibbi.svg
├── cat.svg
├── cheese-cat-collaborator.svg
├── cheese-cat.svg
├── dessert-fox-collaborator.svg
├── dessert-fox-rudolph.svg
├── dessert-fox.svg
├── fishman-glasses.svg
├── fishman.svg
├── flamingo.svg
├── galchi-cat.svg
├── ghost-collaborator.svg
├── ghost-king.svg
├── ghost.svg
├── goblin-bag.svg
├── goblin.svg
├── goose-java.svg
├── goose-js.svg
├── goose-kotlin.svg
├── goose-linux.svg
├── goose-node.svg
├── goose-spring.svg
├── goose-sunglasses.svg
├── goose-swift.svg
├── goose.svg
├── hamster-collaborator.svg
├── hamster-java.svg
├── hamster-js.svg
├── hamster-kotlin.svg
├── hamster-santa.svg
├── hamster-spring.svg
├── hamster-tube.svg
├── hamster.svg
├── little-chick-java.svg
├── little-chick-js.svg
├── little-chick-kotlin.svg
├── little-chick-linux.svg
├── little-chick-node.svg
├── little-chick-santa.svg
├── little-chick-spring.svg
├── little-chick-sunglasses.svg
├── little-chick-swift.svg
├── little-chick-tube.svg
├── little-chick.svg
├── logo.svg
├── maltese-collaborator.svg
├── maltese-king.svg
├── maltese.svg
├── mole-grass.svg
├── mole.svg
├── penguin-java.svg
├── penguin-js.svg
├── penguin-kotlin.svg
├── penguin-linux.svg
├── penguin-node.svg
├── penguin-spring.svg
├── penguin-sunglasses.svg
├── penguin-swift.svg
├── penguin.svg
├── pets.svg
├── pig-collaborator.svg
├── pig-java.svg
├── pig-js.svg
├── pig-kotlin.svg
├── pig-linux.svg
├── pig-node.svg
├── pig-spring.svg
├── pig-sunglasses.svg
├── pig-swift.svg
├── pig.svg
├── quokka-leaf.svg
├── quokka-sunglasses.svg
├── quokka.svg
├── rabbit-brown-rudolph.svg
├── rabbit-collaborator.svg
├── rabbit-tube.svg
├── rabbit.svg
├── sample.svg
├── scream-ghost.svg
├── scream.svg
├── shiba-king.svg
├── shiba.svg
├── slime-blue.svg
├── slime-green.svg
├── slime-pumpkin-1.svg
├── slime-pumpkin-2.svg
├── slime-red-java.svg
├── slime-red-js.svg
├── slime-red-kotlin.svg
├── slime-red-linux.svg
├── slime-red-node.svg
├── slime-red-swift.svg
├── slime-red.svg
├── sloth-king.svg
├── sloth-sunglasses.svg
├── sloth.svg
├── snowman-melt.svg
├── snowman.svg
├── tenmm.svg
├── turtle.svg
├── unicorn.svg
├── white-cat-collaborator.svg
└── white-cat.svg
├── gradle.properties
├── gradle
├── core.gradle
├── db.gradle
├── jetbrains.gradle
├── logging.gradle
├── monitor.gradle
├── slack.gradle
├── spring.gradle
├── test.gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
├── kotlin
│ └── org
│ │ └── gitanimals
│ │ ├── Application.kt
│ │ ├── core
│ │ ├── AggregateRoot.kt
│ │ ├── DomainEventPublisher.kt
│ │ ├── ErrorResponse.kt
│ │ ├── Exception.kt
│ │ ├── FieldType.kt
│ │ ├── GracefulShutdownDispatcher.kt
│ │ ├── HttpClientErrorHandler.kt
│ │ ├── IdGenerator.kt
│ │ ├── JacksonConfig.kt
│ │ ├── Mode.kt
│ │ ├── OrchestratorExtension.kt
│ │ ├── PersonaType.kt
│ │ ├── Svgs.kt
│ │ ├── UpdateUserOrchestrator.kt
│ │ ├── advice
│ │ │ └── GlobalExceptionHandler.kt
│ │ ├── appender
│ │ │ └── SlackAppender.kt
│ │ ├── auth
│ │ │ ├── InternalAuth.kt
│ │ │ ├── InternalAuthClient.kt
│ │ │ ├── InternalAuthRequestInterceptor.kt
│ │ │ ├── RequiredUserEntryPointAspect.kt
│ │ │ ├── RequiredUserEntryPoints.kt
│ │ │ └── UserEntryPointValidationExtension.kt
│ │ ├── clock.kt
│ │ ├── extension
│ │ │ ├── HttpResponseExtension.kt
│ │ │ └── StringExtension.kt
│ │ ├── filter
│ │ │ ├── CorsFilter.kt
│ │ │ └── MDCFilter.kt
│ │ ├── interceptor
│ │ │ ├── InterceptorConfigurer.kt
│ │ │ └── InternalApiInterceptor.kt
│ │ ├── lock
│ │ │ ├── DistributedLock.kt
│ │ │ ├── LockAcquireFailException.kt
│ │ │ └── RedisDistributedLockService.kt
│ │ └── redis
│ │ │ ├── AsyncRedisPubSubEvent.kt
│ │ │ ├── RedisConfiguration.kt
│ │ │ ├── RedisPubSubChannel.kt
│ │ │ ├── RedisPubSubEventListener.kt
│ │ │ ├── TraceableMessageListener.kt
│ │ │ ├── TransactionCommitRedisPubSubEvent.kt
│ │ │ └── TransactionCommitRedisPubSubEventListener.kt
│ │ ├── guild
│ │ ├── app
│ │ │ ├── AcceptJoinGuildFacade.kt
│ │ │ ├── ChangeGuildFacade.kt
│ │ │ ├── ChangeMainPersonaFacade.kt
│ │ │ ├── CreateGuildFacade.kt
│ │ │ ├── DenyJoinGuildFacade.kt
│ │ │ ├── DrawGuildFacade.kt
│ │ │ ├── GetJoinedGuildFacade.kt
│ │ │ ├── IdentityApi.kt
│ │ │ ├── JoinGuildFacade.kt
│ │ │ ├── KickGuildFacade.kt
│ │ │ ├── LeaveGuildFacade.kt
│ │ │ ├── RenderApi.kt
│ │ │ ├── SearchGuildFacade.kt
│ │ │ ├── event
│ │ │ │ └── InboxInputEvent.kt
│ │ │ ├── request
│ │ │ │ └── CreateGuildRequest.kt
│ │ │ └── response
│ │ │ │ ├── GuildBackgroundResponse.kt
│ │ │ │ ├── GuildIconsResponse.kt
│ │ │ │ ├── GuildPagingResponse.kt
│ │ │ │ ├── GuildResponse.kt
│ │ │ │ └── GuildsResponse.kt
│ │ ├── controller
│ │ │ ├── GuildController.kt
│ │ │ └── request
│ │ │ │ └── JoinGuildRequest.kt
│ │ ├── domain
│ │ │ ├── AbstractTime.kt
│ │ │ ├── Guild.kt
│ │ │ ├── GuildIcons.kt
│ │ │ ├── GuildRepository.kt
│ │ │ ├── GuildService.kt
│ │ │ ├── Leader.kt
│ │ │ ├── Member.kt
│ │ │ ├── RandomGuildCache.kt
│ │ │ ├── SearchFilter.kt
│ │ │ ├── WaitMember.kt
│ │ │ ├── event
│ │ │ │ └── GuildContributionUpdated.kt
│ │ │ ├── extension
│ │ │ │ └── GuildFieldTypeExtension.kt
│ │ │ └── request
│ │ │ │ ├── ChangeGuildRequest.kt
│ │ │ │ └── CreateLeaderRequest.kt
│ │ ├── infra
│ │ │ ├── GuildHttpClientConfigurer.kt
│ │ │ ├── GuildRedisEventSubscriber.kt
│ │ │ ├── GuildUpdateGuildContributionMessageListener.kt
│ │ │ ├── HttpClientErrorHandler.kt
│ │ │ ├── InMemoryRandomGuildCache.kt
│ │ │ └── event
│ │ │ │ └── UserContributionUpdated.kt
│ │ └── saga
│ │ │ ├── PersonaDeletedSagaHandler.kt
│ │ │ └── event
│ │ │ └── PersonaDeleted.kt
│ │ ├── rank
│ │ ├── app
│ │ │ ├── GetRankByUsernameFacade.kt
│ │ │ ├── GivePointToGuildFacade.kt
│ │ │ ├── GivePointToUsersFacade.kt
│ │ │ ├── GuildApi.kt
│ │ │ ├── IdentityApi.kt
│ │ │ ├── RankQueryFacade.kt
│ │ │ └── RenderApi.kt
│ │ ├── controller
│ │ │ ├── RankController.kt
│ │ │ └── response
│ │ │ │ ├── RankHistoryResponse.kt
│ │ │ │ ├── RankResponses.kt
│ │ │ │ └── RankTotalCountResponse.kt
│ │ ├── domain
│ │ │ ├── AbstractTime.kt
│ │ │ ├── GuildContributionRank.kt
│ │ │ ├── GuildContributionRankRepository.kt
│ │ │ ├── GuildContributionRankService.kt
│ │ │ ├── Rank.kt
│ │ │ ├── RankQueryRepository.kt
│ │ │ ├── UserContributionRank.kt
│ │ │ ├── UserContributionRankRepository.kt
│ │ │ ├── UserContributionRankService.kt
│ │ │ ├── event
│ │ │ │ └── RankUpdated.kt
│ │ │ ├── history
│ │ │ │ ├── RankHistory.kt
│ │ │ │ ├── RankHistoryRepository.kt
│ │ │ │ ├── RankHistoryService.kt
│ │ │ │ ├── Winner.kt
│ │ │ │ └── request
│ │ │ │ │ └── InitRankHistoryRequest.kt
│ │ │ └── response
│ │ │ │ └── RankResponse.kt
│ │ └── infra
│ │ │ ├── HttpClientErrorHandler.kt
│ │ │ ├── RankCacheConfigurer.kt
│ │ │ ├── RankHttpClientConfigurer.kt
│ │ │ ├── RankRedisEventSubscriber.kt
│ │ │ ├── RankUpdateGuildContributionMessageListener.kt
│ │ │ ├── RedisRankQueryRepository.kt
│ │ │ ├── UpdateUserContributionMessageListener.kt
│ │ │ └── event
│ │ │ ├── GuildContributionUpdated.kt
│ │ │ └── UserContributionUpdated.kt
│ │ ├── render
│ │ ├── app
│ │ │ ├── AnimationFacade.kt
│ │ │ ├── ContributionApi.kt
│ │ │ ├── GithubRestApi.kt
│ │ │ ├── IdentityApi.kt
│ │ │ ├── UserFacade.kt
│ │ │ ├── UserStatisticSchedule.kt
│ │ │ └── request
│ │ │ │ └── MergePersonaRequest.kt
│ │ ├── controller
│ │ │ ├── AnimationController.kt
│ │ │ ├── BackgroundController.kt
│ │ │ ├── InternalAnimationController.kt
│ │ │ ├── InternalPersonaController.kt
│ │ │ ├── PersonaController.kt
│ │ │ ├── PersonaStatisticController.kt
│ │ │ ├── UserStatisticController.kt
│ │ │ ├── request
│ │ │ │ ├── AddMultiplyPersonaRequest.kt
│ │ │ │ ├── AddPersonaRequest.kt
│ │ │ │ ├── ChangeFieldRequest.kt
│ │ │ │ └── UsernameAndPersonaIdRequest.kt
│ │ │ └── response
│ │ │ │ ├── BackgroundResponse.kt
│ │ │ │ ├── PersonaEnumResponse.kt
│ │ │ │ ├── PersonaResponse.kt
│ │ │ │ ├── TotalPersonaResponse.kt
│ │ │ │ ├── TotalUserResponse.kt
│ │ │ │ └── UserResponse.kt
│ │ ├── domain
│ │ │ ├── AbstractTime.kt
│ │ │ ├── EntryPoint.kt
│ │ │ ├── Field.kt
│ │ │ ├── Idempotency.kt
│ │ │ ├── IdempotencyRepository.kt
│ │ │ ├── Persona.kt
│ │ │ ├── PersonaStatisticRepository.kt
│ │ │ ├── PersonaStatisticService.kt
│ │ │ ├── User.kt
│ │ │ ├── UserAuthInfo.kt
│ │ │ ├── UserRepository.kt
│ │ │ ├── UserService.kt
│ │ │ ├── UserStatisticRepository.kt
│ │ │ ├── UserStatisticService.kt
│ │ │ ├── event
│ │ │ │ ├── NewUserCreated.kt
│ │ │ │ ├── PersonaDeleted.kt
│ │ │ │ ├── UserContributionUpdated.kt
│ │ │ │ ├── UserYesterdayReport.kt
│ │ │ │ ├── UsernameChanged.kt
│ │ │ │ └── Visited.kt
│ │ │ ├── extension
│ │ │ │ └── RenderFieldTypeExtension.kt
│ │ │ ├── request
│ │ │ │ └── PersonaChangeRequest.kt
│ │ │ ├── response
│ │ │ │ ├── NewPetDropRateDistribution.kt
│ │ │ │ └── PersonaResponse.kt
│ │ │ └── value
│ │ │ │ ├── Contribution.kt
│ │ │ │ └── Level.kt
│ │ ├── infra
│ │ │ ├── CacheConfigurer.kt
│ │ │ ├── CustomExecutorConfigurer.kt
│ │ │ ├── GithubContributionApi.kt
│ │ │ ├── HttpClientErrorHandler.kt
│ │ │ ├── NewPetDropRateDistributionReport.kt
│ │ │ ├── PersonaDeletedEventHandler.kt
│ │ │ ├── RenderHttpClientConfigurer.kt
│ │ │ ├── VisitedEventListener.kt
│ │ │ └── event
│ │ │ │ └── NewPetDropRateDistributionEvent.kt
│ │ └── saga
│ │ │ ├── UsedCouponSagaHandlers.kt
│ │ │ └── event
│ │ │ ├── CouponUsed.kt
│ │ │ └── GavePoint.kt
│ │ ├── star
│ │ ├── controller
│ │ │ └── StargazerController.kt
│ │ ├── domain
│ │ │ ├── Stargazer.kt
│ │ │ ├── StargazerService.kt
│ │ │ └── StargazersRepository.kt
│ │ └── infra
│ │ │ ├── GithubStargazerApi.kt
│ │ │ └── StargazerBatchJob.kt
│ │ └── supports
│ │ ├── deadletter
│ │ ├── DeadLetterEvent.kt
│ │ ├── DeadLetterEventPublisher.kt
│ │ ├── DeadLetterListenConfigurer.kt
│ │ ├── DeadLetterRedisMessageListenerConfiguration.kt
│ │ └── DeadLetterRelayEventListener.kt
│ │ ├── event
│ │ ├── SlackInteracted.kt
│ │ └── SlackReplied.kt
│ │ └── orchestrate
│ │ ├── IdentityApi.kt
│ │ ├── NetxUserOrchestrator.kt
│ │ └── SupportsOrchestrateHttpClientConfigurer.kt
└── resources
│ ├── application.properties
│ ├── github-graphql
│ ├── contribution-count-by-year.graphql
│ ├── contribution-year.graphql
│ ├── schema.docs.graphql
│ └── stargazer.graphql
│ ├── logback.xml
│ └── persona
│ ├── animal
│ ├── bbibbi.svg
│ ├── cat.svg
│ ├── cheese-cat-collaborator.svg
│ ├── cheese-cat.svg
│ ├── dessert-fox-collaborator.svg
│ ├── dessert-fox-rudolph.svg
│ ├── dessert-fox.svg
│ ├── fishman-glasses.svg
│ ├── fishman.svg
│ ├── flamingo.svg
│ ├── galchi-cat.svg
│ ├── ghost-collaborator.svg
│ ├── ghost-king.svg
│ ├── ghost.svg
│ ├── goblin-bag.svg
│ ├── goblin.svg
│ ├── goose-java.svg
│ ├── goose-js.svg
│ ├── goose-kotlin.svg
│ ├── goose-linux.svg
│ ├── goose-node.svg
│ ├── goose-spring.svg
│ ├── goose-sunglasses.svg
│ ├── goose-swift.svg
│ ├── goose.svg
│ ├── hamster-collaborator.svg
│ ├── hamster-java.svg
│ ├── hamster-js.svg
│ ├── hamster-kotlin.svg
│ ├── hamster-santa.svg
│ ├── hamster-spring.svg
│ ├── hamster-tube.svg
│ ├── hamster.svg
│ ├── little-chick-java.svg
│ ├── little-chick-js.svg
│ ├── little-chick-kotlin.svg
│ ├── little-chick-linux.svg
│ ├── little-chick-node.svg
│ ├── little-chick-santa.svg
│ ├── little-chick-spring.svg
│ ├── little-chick-sunglasses.svg
│ ├── little-chick-swift.svg
│ ├── little-chick-tube.svg
│ ├── little-chick.svg
│ ├── maltese-collaborator.svg
│ ├── maltese-king.svg
│ ├── maltese.svg
│ ├── mole-grass.svg
│ ├── mole.svg
│ ├── penguin-java.svg
│ ├── penguin-js.svg
│ ├── penguin-kotlin.svg
│ ├── penguin-linux.svg
│ ├── penguin-node.svg
│ ├── penguin-spring.svg
│ ├── penguin-sunglasses.svg
│ ├── penguin-swift.svg
│ ├── penguin.svg
│ ├── pig-collaborator.svg
│ ├── pig-java.svg
│ ├── pig-js.svg
│ ├── pig-kotlin.svg
│ ├── pig-linux.svg
│ ├── pig-node.svg
│ ├── pig-spring.svg
│ ├── pig-sunglasses.svg
│ ├── pig-swift.svg
│ ├── pig.svg
│ ├── quokka-leaf.svg
│ ├── quokka-sunglasses.svg
│ ├── quokka.svg
│ ├── rabbit-brown-rudolph.svg
│ ├── rabbit-collaborator.svg
│ ├── rabbit-tube.svg
│ ├── rabbit.svg
│ ├── scream-ghost.svg
│ ├── scream.svg
│ ├── shiba-king.svg
│ ├── shiba.svg
│ ├── slime-blue.svg
│ ├── slime-green.svg
│ ├── slime-pumpkin-1.svg
│ ├── slime-pumpkin-2.svg
│ ├── slime-red-java.svg
│ ├── slime-red-js.svg
│ ├── slime-red-kotlin.svg
│ ├── slime-red-linux.svg
│ ├── slime-red-node.svg
│ ├── slime-red-swift.svg
│ ├── slime-red.svg
│ ├── sloth-king.svg
│ ├── sloth-sunglasses.svg
│ ├── sloth.svg
│ ├── snowman-melt.svg
│ ├── snowman.svg
│ ├── tenmm.svg
│ ├── turtle.svg
│ ├── unicorn.svg
│ ├── white-cat-collaborator.svg
│ └── white-cat.svg
│ ├── field
│ ├── carrot-and-coin.svg
│ ├── folder.svg
│ ├── grass-christmastree-background.svg
│ ├── grass-christmastree-field.svg
│ ├── grass-field.svg
│ ├── halloween-field.svg
│ ├── logo-showing.svg
│ ├── red-computer.svg
│ ├── red-sofa.svg
│ ├── snow-grass-background.svg
│ ├── snow-grass-field.svg
│ ├── snow-house-background.svg
│ ├── snow-house-field.svg
│ ├── snowy-field.svg
│ └── white-field.svg
│ └── text
│ ├── large
│ ├── 0.svg
│ ├── 1.svg
│ ├── 2.svg
│ ├── 3.svg
│ ├── 4.svg
│ ├── 5.svg
│ ├── 6.svg
│ ├── 7.svg
│ ├── 8.svg
│ ├── 9.svg
│ ├── A.svg
│ ├── B.svg
│ ├── C.svg
│ ├── D.svg
│ ├── E.svg
│ ├── F.svg
│ ├── G.svg
│ ├── H.svg
│ ├── I.svg
│ ├── J.svg
│ ├── K.svg
│ ├── L.svg
│ ├── M.svg
│ ├── N.svg
│ ├── O.svg
│ ├── P.svg
│ ├── Q.svg
│ ├── R.svg
│ ├── S.svg
│ ├── T.svg
│ ├── U.svg
│ ├── V.svg
│ ├── W.svg
│ ├── X.svg
│ ├── Y.svg
│ ├── Z.svg
│ ├── _a.svg
│ ├── _b.svg
│ ├── _c.svg
│ ├── _d.svg
│ ├── _e.svg
│ ├── _f.svg
│ ├── _g.svg
│ ├── _h.svg
│ ├── _i.svg
│ ├── _j.svg
│ ├── _k.svg
│ ├── _l.svg
│ ├── _m.svg
│ ├── _n.svg
│ ├── _o.svg
│ ├── _p.svg
│ ├── _q.svg
│ ├── _r.svg
│ ├── _s.svg
│ ├── _t.svg
│ ├── _u.svg
│ ├── _v.svg
│ ├── _w.svg
│ ├── _x.svg
│ ├── _y.svg
│ ├── _z.svg
│ └── hyphens.svg
│ ├── medium
│ ├── 0.svg
│ ├── 1.svg
│ ├── 2.svg
│ ├── 3.svg
│ ├── 4.svg
│ ├── 5.svg
│ ├── 6.svg
│ ├── 7.svg
│ ├── 8.svg
│ └── 9.svg
│ └── small
│ ├── 0.svg
│ ├── 1.svg
│ ├── 2.svg
│ ├── 3.svg
│ ├── 4.svg
│ ├── 5.svg
│ ├── 6.svg
│ ├── 7.svg
│ ├── 8.svg
│ └── 9.svg
└── test
├── kotlin
└── org
│ └── gitanimals
│ ├── core
│ ├── auth
│ │ └── InternalAuthTest.kt
│ └── extension
│ │ └── StringExtensionTest.kt
│ ├── guild
│ ├── app
│ │ └── CreateGuildFacadeTest.kt
│ ├── domain
│ │ ├── Fixture.kt
│ │ └── GuildServiceTest.kt
│ ├── saga
│ │ └── PersonaDeletedSagaHandlerTest.kt
│ └── supports
│ │ ├── GuildSagaCapture.kt
│ │ ├── MockApiConfiguration.kt
│ │ └── RedisContainer.kt
│ ├── render
│ ├── app
│ │ ├── AnimationFacadeTest.kt
│ │ └── UserStatisticScheduleTest.kt
│ ├── domain
│ │ ├── PersonaStatisticServiceTest.kt
│ │ ├── UserFixture.kt
│ │ ├── UserServiceTest.kt
│ │ ├── UserStatisticsServiceTest.kt
│ │ └── UserTest.kt
│ ├── saga
│ │ └── UsedCouponSagaHandlerTest.kt
│ └── supports
│ │ ├── DomainEventHolder.kt
│ │ ├── IntegrationTest.kt
│ │ ├── RedisContainer.kt
│ │ └── SagaCapture.kt
│ └── supports
│ └── orchestrate
│ └── NetxUserOrchestratorTest.kt
└── resources
├── application.properties
└── persona
└── goose
├── test.html
└── test.svg
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @devxb
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "application"
3 | id "org.jetbrains.kotlin.jvm" version "${jetbrainKotlinVersion}"
4 | id "org.jetbrains.kotlin.plugin.spring" version "${jetbrainKotlinVersion}"
5 | id 'org.jetbrains.kotlin.plugin.jpa' version "${jetbrainKotlinVersion}"
6 | id "org.springframework.boot" version "${springbootVersion}"
7 | id "io.spring.dependency-management" version "${springDependencyManagementVersion}"
8 | id "io.sentry.jvm.gradle" version "${sentryVersion}"
9 | }
10 |
11 | group = "${group}"
12 | version = "${version}"
13 |
14 | repositories {
15 | mavenCentral()
16 | maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" }
17 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
18 | maven { url "https://s01.oss.sonatype.org/content/groups/staging/" }
19 | }
20 |
21 | test {
22 | useJUnitPlatform()
23 | }
24 |
25 | application {
26 | mainClassName = 'org.gitanimals.Application'
27 | }
28 |
29 | sentry {
30 | includeSourceContext = true
31 |
32 | org = "devxb"
33 | projectName = "gitanimals-render"
34 | authToken = System.getProperty("SENTRY_AUTH_TOKEN")
35 | }
36 |
37 | apply from: "gradle/db.gradle"
38 | apply from: "gradle/core.gradle"
39 | apply from: "gradle/test.gradle"
40 | apply from: "gradle/slack.gradle"
41 | apply from: "gradle/spring.gradle"
42 | apply from: "gradle/monitor.gradle"
43 | apply from: "gradle/logging.gradle"
44 | apply from: "gradle/jetbrains.gradle"
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | ### Project ###
4 | group=org.gitanimals
5 | version=1.0
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 |
--------------------------------------------------------------------------------
/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 | }
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradle/logging.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation "co.elastic.logging:logback-ecs-encoder:${logbackEcsEncoderVersion}"
3 | }
4 |
--------------------------------------------------------------------------------
/gradle/monitor.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
3 | }
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-goods/gitanimals/bb981af8fff02bed5f850027a17c7539ba7f44a3/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 |
2 | rootProject.name = 'gitanimals'
3 |
4 |
--------------------------------------------------------------------------------
/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/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/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/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/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/core/GracefulShutdownDispatcher.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core
2 |
3 | import jakarta.annotation.PreDestroy
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.asCoroutineDispatcher
6 | import org.gitanimals.core.GracefulShutdownDispatcher.executorService
7 | import org.slf4j.LoggerFactory
8 | import org.springframework.stereotype.Component
9 | import java.util.concurrent.Executors
10 | import java.util.concurrent.TimeUnit
11 |
12 | object GracefulShutdownDispatcher {
13 |
14 | val executorService = Executors.newFixedThreadPool(10) { runnable ->
15 | Thread(runnable, "gitanimals-gracefulshutdown").apply { isDaemon = false }
16 | }
17 |
18 | val dispatcher: CoroutineDispatcher = executorService.asCoroutineDispatcher()
19 | }
20 |
21 | @Component
22 | class GracefulShutdownHook {
23 |
24 | private val logger = LoggerFactory.getLogger(this::class.simpleName)
25 |
26 | @PreDestroy
27 | fun tryGracefulShutdown() {
28 | logger.info("Shutting down dispatcher...")
29 | executorService.shutdown()
30 | runCatching {
31 | if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
32 | logger.warn("Forcing shutdown...")
33 | executorService.shutdownNow()
34 | } else {
35 | logger.info("Shutdown completed gracefully.")
36 | }
37 | }.onFailure {
38 | if (it is InterruptedException) {
39 | logger.warn("Shutdown interrupted. Forcing shutdown...")
40 | executorService.shutdownNow()
41 | Thread.currentThread().interrupt()
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/core/auth/InternalAuthRequestInterceptor.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.auth
2 |
3 | import org.gitanimals.core.filter.MDCFilter.Companion.USER_ENTRY_POINT
4 | import org.gitanimals.core.filter.MDCFilter.Companion.USER_ID
5 | import org.slf4j.MDC
6 | import org.springframework.http.HttpRequest
7 | import org.springframework.http.client.ClientHttpRequestExecution
8 | import org.springframework.http.client.ClientHttpRequestInterceptor
9 | import org.springframework.http.client.ClientHttpResponse
10 | import org.springframework.stereotype.Component
11 | import java.util.*
12 |
13 | @Component
14 | class InternalAuthRequestInterceptor(
15 | private val internalAuth: InternalAuth,
16 | ) : ClientHttpRequestInterceptor {
17 |
18 | override fun intercept(
19 | request: HttpRequest,
20 | body: ByteArray,
21 | execution: ClientHttpRequestExecution
22 | ): ClientHttpResponse {
23 | val userId = runCatching {
24 | MDC.get(USER_ID).toLong()
25 | }.getOrNull()
26 |
27 | if (userId != null) {
28 | val encrypt = internalAuth.encrypt(userId = userId)
29 |
30 | request.headers.add(
31 | InternalAuth.INTERNAL_AUTH_SECRET_KEY,
32 | Base64.getEncoder().encodeToString(encrypt.secret),
33 | )
34 | request.headers.add(
35 | InternalAuth.INTERNAL_AUTH_IV_KEY,
36 | Base64.getEncoder().encodeToString(encrypt.iv),
37 | )
38 | }
39 |
40 | val userEntryPoint = runCatching {
41 | MDC.get(USER_ENTRY_POINT)
42 | }.getOrNull()
43 |
44 | if (userEntryPoint != null) {
45 | request.headers.add(
46 | InternalAuth.INTERNAL_ENTRY_POINT_KEY,
47 | userEntryPoint,
48 | )
49 | }
50 |
51 | return execution.execute(request, body)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/gitanimals/core/lock/DistributedLock.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.lock
2 |
3 | import org.springframework.stereotype.Component
4 | import kotlin.time.Duration.Companion.seconds
5 |
6 | object LOCK_KEY_PREFIX {
7 | const val CREATE_NEW_USER = "CREATE_NEW_USER"
8 | const val SET_AUTH = "SET_AUTH"
9 | }
10 |
11 | object DistributedLock {
12 |
13 | private lateinit var lockService: DistributedLockService
14 |
15 | fun withLock(
16 | key: String,
17 | leaseMillis: Long = 10.seconds.inWholeMilliseconds,
18 | waitMillis: Long = 3.seconds.inWholeMilliseconds,
19 | action: () -> T,
20 | ): T {
21 | return lockService.withLock(
22 | key = key,
23 | leaseMillis = leaseMillis,
24 | waitMillis = waitMillis,
25 | action = action,
26 | )
27 | }
28 |
29 | interface DistributedLockService {
30 |
31 | fun withLock(
32 | key: String,
33 | leaseMillis: Long = 10.seconds.inWholeMilliseconds,
34 | waitMillis: Long = 3.seconds.inWholeMilliseconds,
35 | action: () -> T,
36 | ): T
37 | }
38 |
39 | @Component
40 | class DistributedLockServiceInjector(distributedLockService: DistributedLockService) {
41 |
42 | init {
43 | lockService = distributedLockService
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/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/kotlin/org/gitanimals/core/lock/RedisDistributedLockService.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.lock
2 |
3 | import org.redisson.Redisson
4 | import org.redisson.api.RedissonClient
5 | import org.redisson.config.Config
6 | import org.springframework.beans.factory.annotation.Value
7 | import org.springframework.context.annotation.Bean
8 | import org.springframework.context.annotation.Configuration
9 | import org.springframework.stereotype.Component
10 | import java.util.concurrent.TimeUnit
11 |
12 | @Component
13 | class RedisDistributedLockService(
14 | private val redissonClient: RedissonClient,
15 | ) : DistributedLock.DistributedLockService {
16 |
17 | override fun withLock(
18 | key: String,
19 | leaseMillis: Long,
20 | waitMillis: Long,
21 | action: () -> T
22 | ): T {
23 | val lock = redissonClient.getLock(key)
24 | val acquired = lock.tryLock(waitMillis, leaseMillis, TimeUnit.MILLISECONDS)
25 |
26 | return if (acquired) {
27 | runCatching {
28 | action.invoke()
29 | }.getOrElse {
30 | throw it
31 | }.also {
32 | lock.unlock()
33 | }
34 | } else {
35 | throw LockAcquireFailException(message = "Cannot acquire lock")
36 | }
37 | }
38 | }
39 |
40 | @Configuration
41 | class RedissonConfig(
42 | @Value("\${netx.host}") private val host: String,
43 | @Value("\${netx.port}") private val port: String,
44 | @Value("\${netx.password:0000}") private val password: String,
45 | ) {
46 |
47 | @Bean
48 | fun redissonClient(): RedissonClient {
49 | val config = Config()
50 |
51 | config.useSingleServer()
52 | .setAddress("redis://$host:$port")
53 | // .setPassword(password)
54 |
55 | return Redisson.create(config)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/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/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/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/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/TraceableMessageListener.kt:
--------------------------------------------------------------------------------
1 | package org.gitanimals.core.redis
2 |
3 | import com.fasterxml.jackson.core.type.TypeReference
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import org.gitanimals.core.filter.MDCFilter
6 | import org.gitanimals.core.filter.MDCFilter.Companion.TRACE_ID
7 | import org.slf4j.LoggerFactory
8 | import org.slf4j.MDC
9 | import org.springframework.data.redis.connection.Message
10 | import org.springframework.data.redis.connection.MessageListener
11 | import org.springframework.data.redis.core.StringRedisTemplate
12 |
13 | abstract class TraceableMessageListener(
14 | private val redisTemplate: StringRedisTemplate,
15 | private val objectMapper: ObjectMapper,
16 | ) : MessageListener {
17 |
18 | private val logger = LoggerFactory.getLogger(this::class.simpleName)
19 |
20 | override fun onMessage(message: Message, pattern: ByteArray?) {
21 | runCatching {
22 | val request = objectMapper.readValue(
23 | redisTemplate.stringSerializer.deserialize(message.body),
24 | object : TypeReference