├── README.md ├── settings.gradle ├── src ├── .DS_Store ├── main │ ├── .DS_Store │ ├── java │ │ ├── .DS_Store │ │ └── com │ │ │ ├── .DS_Store │ │ │ └── huseyin │ │ │ └── youcontribute │ │ │ ├── exceptions │ │ │ ├── ErrorResponse.java │ │ │ ├── DuplicatedRepositoryException.java │ │ │ └── GlobalExceptionHandler.java │ │ │ ├── config │ │ │ ├── RestTemplateConfig.java │ │ │ ├── WebConfig.java │ │ │ ├── GithubProperties.java │ │ │ ├── CorsConfiguration.java │ │ │ ├── ApplicationProperties.java │ │ │ └── AsyncConfig.java │ │ │ ├── controllers │ │ │ ├── requests │ │ │ │ ├── CreateRepositoryRequest.java │ │ │ │ └── UpdateChallengeStatusRequest.java │ │ │ ├── resources │ │ │ │ ├── RepositoryResource.java │ │ │ │ ├── IssueResource.java │ │ │ │ └── IssueChallengeResource.java │ │ │ ├── RepositoryController.java │ │ │ ├── IssueController.java │ │ │ └── IssueChallengeController.java │ │ │ ├── Application.java │ │ │ ├── repositories │ │ │ ├── RepositoryRepository.java │ │ │ ├── IssueChallengeRepository.java │ │ │ └── IssueRepository.java │ │ │ ├── service │ │ │ ├── models │ │ │ │ ├── GithubPullResponse.java │ │ │ │ └── GithubIssueResponse.java │ │ │ ├── IssueService.java │ │ │ ├── IssueChallengeService.java │ │ │ ├── RepositoryService.java │ │ │ └── GithubClient.java │ │ │ ├── models │ │ │ ├── IssueChallengeStatus.java │ │ │ ├── IssueChallenge.java │ │ │ ├── Repository.java │ │ │ └── Issue.java │ │ │ ├── schedulers │ │ │ ├── ImportIssuesScheduler.java │ │ │ ├── ChallengeIssuesScheduler.java │ │ │ └── TrackChallengesScheduler.java │ │ │ ├── clients │ │ │ └── OneSignalClient.java │ │ │ └── managers │ │ │ └── RepositoryManager.java │ └── resources │ │ └── application.yml └── test │ ├── java │ └── com │ │ └── huseyin │ │ └── youcontribute │ │ ├── ApplicationTests.java │ │ ├── clients │ │ └── OneSignalClientTest.java │ │ ├── controllers │ │ └── RepositoryControllerTest.java │ │ ├── repositories │ │ ├── RepositoryRepositoryTestIT.java │ │ └── IssueRepositoryTestIT.java │ │ └── service │ │ └── GithubClientServiceTests.java │ └── resources │ ├── application.yml │ └── __files │ └── github │ └── issues.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── Dockerfile ├── .gitignore ├── k8s.yaml ├── gradlew.bat └── gradlew /README.md: -------------------------------------------------------------------------------- 1 | test delete 2 | twitch stream issue fixed 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'youcontribute' 2 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huseyinbabal/youcontribute/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huseyinbabal/youcontribute/HEAD/src/main/.DS_Store -------------------------------------------------------------------------------- /src/main/java/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huseyinbabal/youcontribute/HEAD/src/main/java/.DS_Store -------------------------------------------------------------------------------- /src/main/java/com/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huseyinbabal/youcontribute/HEAD/src/main/java/com/.DS_Store -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huseyinbabal/youcontribute/HEAD/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-7.0.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/test/java/com/huseyin/youcontribute/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:15-jdk-alpine as builder 2 | RUN mkdir -p /usr/src/app 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | RUN ./gradlew build -x test 6 | 7 | FROM adoptopenjdk/openjdk15:jre-15.0.2_7-alpine as runner 8 | COPY --from=builder /usr/src/app/build/libs/*.jar /app.jar 9 | EXPOSE 8080 10 | ENTRYPOINT ["java", "-jar", "/app.jar"] 11 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/exceptions/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.exceptions; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | 9 | @Data 10 | @Builder 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | public class ErrorResponse { 13 | 14 | private String message; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.client.RestTemplate; 6 | 7 | @Configuration 8 | public class RestTemplateConfig { 9 | 10 | @Bean 11 | public RestTemplate restTemplate() { 12 | return new RestTemplate(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/requests/CreateRepositoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers.requests; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @Builder 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class CreateRepositoryRequest { 13 | 14 | private String organization; 15 | private String repository; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/requests/UpdateChallengeStatusRequest.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers.requests; 2 | 3 | import com.huseyin.youcontribute.models.IssueChallengeStatus; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class UpdateChallengeStatusRequest { 14 | 15 | private IssueChallengeStatus status; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/exceptions/DuplicatedRepositoryException.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.exceptions; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class DuplicatedRepositoryException extends RuntimeException { 9 | 10 | public DuplicatedRepositoryException(String organization, String repository) { 11 | super(String.format("Organization: %s Repository: %s already exists.", organization, repository)); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/Application.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute; 2 | 3 | import com.huseyin.youcontribute.models.IssueChallengeStatus; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | @SpringBootApplication 9 | @EnableScheduling 10 | public class Application { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | @EnableWebMvc 10 | public class WebConfig implements WebMvcConfigurer { 11 | 12 | @Override 13 | public void addCorsMappings(CorsRegistry registry) { 14 | registry.addMapping("/**"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/repositories/RepositoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.repositories; 2 | 3 | import com.huseyin.youcontribute.models.Repository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.data.repository.PagingAndSortingRepository; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface RepositoryRepository extends PagingAndSortingRepository { 11 | 12 | List findAll(); 13 | 14 | Optional findByOrganizationAndRepository(String organization, String repository); 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driverClassName: org.h2.Driver 4 | url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 5 | username: sa 6 | password: sa 7 | jpa: 8 | hibernate: 9 | ddl-auto: create 10 | database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 11 | --- 12 | spring: 13 | profiles: it 14 | datasource: 15 | driverClassName: com.mysql.cj.jdbc.Driver 16 | jpa: 17 | hibernate: 18 | ddl-auto: create 19 | database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/config/GithubProperties.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.config; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @ConfigurationProperties(prefix = "github") 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | @Data 15 | public class GithubProperties { 16 | 17 | private String apiUrl; 18 | 19 | private String token; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/repositories/IssueChallengeRepository.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.repositories; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import com.huseyin.youcontribute.models.IssueChallenge; 7 | import com.huseyin.youcontribute.models.IssueChallengeStatus; 8 | import org.springframework.data.repository.PagingAndSortingRepository; 9 | 10 | public interface IssueChallengeRepository extends PagingAndSortingRepository { 11 | 12 | Optional findByStatusIn(List status); 13 | 14 | @Override 15 | List findAll(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driverClassName: com.mysql.cj.jdbc.Driver 4 | jpa: 5 | hibernate: 6 | ddl-auto: update 7 | database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 8 | github: 9 | api-url: https://api.github.com 10 | token: ${GITHUB_API_TOKEN} 11 | application: 12 | import-frequency: ${ISSUES_IMPORT_FREQUENCY:60000} 13 | challenge-frequency: ${ISSUES_CHALLENGE_FREQUENCY:10000} 14 | challenge-tracking-frequency: ${CHALLENGE_TRACKING_FREQUENCY:10000} 15 | one-signal: 16 | app-id: ${ONE_SIGNAL_APP_ID} 17 | api-auth-key: ${ONE_SIGNAL_API_AUTH_KEY} 18 | new-challenge-template-id: "4f90f941-cdd2-43a4-b932-6ecff434b355" 19 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/service/models/GithubPullResponse.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.service.models; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | @Data 11 | public class GithubPullResponse { 12 | 13 | private String state; 14 | 15 | private User user; 16 | 17 | private String body; 18 | 19 | @Data 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | public static class User { 23 | 24 | private String login; 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/config/CorsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | public class CorsConfiguration { 10 | 11 | @Bean 12 | public WebMvcConfigurer corsConfigurer() { 13 | return new WebMvcConfigurer() { 14 | 15 | @Override 16 | public void addCorsMappings(CorsRegistry registry) { 17 | registry.addMapping("/**").allowedOrigins("*").allowedMethods("*"); 18 | } 19 | }; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/models/IssueChallengeStatus.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.models; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | 10 | @AllArgsConstructor 11 | @Getter 12 | public enum IssueChallengeStatus { 13 | PENDING("PENDING"), 14 | ACCEPTED("ACCEPTED"), 15 | REJECTED("REJECTED"), 16 | COMPLETED("COMPLETED"); 17 | 18 | private String value; 19 | 20 | public static List ongoingStatuses() { 21 | return Arrays.stream(IssueChallengeStatus.values()).filter( 22 | status -> PENDING.equals(status) || ACCEPTED.equals(status)) 23 | .collect(Collectors.toList()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/repositories/IssueRepository.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.repositories; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import com.huseyin.youcontribute.models.Issue; 7 | import com.huseyin.youcontribute.models.Repository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.PagingAndSortingRepository; 10 | 11 | public interface IssueRepository extends PagingAndSortingRepository { 12 | 13 | List findAll(); 14 | 15 | List findByRepository(Repository repository); 16 | 17 | @Query(value = "select * from issue where id not in (select issue_id from issue_challenge) order by rand() limit 1", nativeQuery = true) 18 | Optional findRandomIssue(); 19 | 20 | Optional findByGithubIssueId(Long githubIssueId); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/config/ApplicationProperties.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.config; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @ConfigurationProperties(prefix = "application") 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | @Data 14 | public class ApplicationProperties { 15 | 16 | private Integer importFrequency; 17 | 18 | private Integer challengeFrequency; 19 | 20 | private OneSignalProperties oneSignal; 21 | 22 | @Data 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public static class OneSignalProperties { 26 | 27 | private String apiAuthKey; 28 | 29 | private String newChallengeTemplateId; 30 | 31 | private String appId; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/resources/RepositoryResource.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers.resources; 2 | 3 | import com.huseyin.youcontribute.models.Repository; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | @Data 11 | @Builder 12 | public class RepositoryResource { 13 | 14 | private Integer id; 15 | 16 | private String name; 17 | 18 | private String organization; 19 | 20 | public static RepositoryResource createFor(Repository repository) { 21 | return RepositoryResource.builder() 22 | .id(repository.getId()) 23 | .name(repository.getRepository()) 24 | .organization(repository.getOrganization()) 25 | .build(); 26 | } 27 | 28 | public static List createFor(List repositories) { 29 | return repositories.stream().map(RepositoryResource::createFor).collect(Collectors.toList()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/resources/IssueResource.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers.resources; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import com.huseyin.youcontribute.models.Issue; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | 10 | @Data 11 | @Builder 12 | public class IssueResource { 13 | 14 | private Integer id; 15 | 16 | private Long githubIssueId; 17 | 18 | private Long githubIssueNumber; 19 | 20 | private String title; 21 | 22 | private String body; 23 | 24 | private String url; 25 | 26 | public static IssueResource createFor(Issue issue) { 27 | return IssueResource.builder() 28 | .id(issue.getId()) 29 | .title(issue.getTitle()) 30 | .body(issue.getBody()) 31 | .githubIssueId(issue.getGithubIssueId()) 32 | .url(issue.getUrl()) 33 | .build(); 34 | } 35 | 36 | public static List createFor(List issues) { 37 | return issues.stream().map(IssueResource::createFor).collect(Collectors.toList()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/schedulers/ImportIssuesScheduler.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.schedulers; 2 | 3 | import java.util.List; 4 | 5 | import com.huseyin.youcontribute.managers.RepositoryManager; 6 | import com.huseyin.youcontribute.models.Repository; 7 | import com.huseyin.youcontribute.service.RepositoryService; 8 | import lombok.AllArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | @Slf4j 15 | @AllArgsConstructor 16 | public class ImportIssuesScheduler { 17 | 18 | private final RepositoryService repositoryService; 19 | 20 | private final RepositoryManager repositoryManager; 21 | 22 | @Scheduled(fixedRateString = "${application.import-frequency}", initialDelay = 60000) 23 | public void importIssuesScheduler() { 24 | log.info("Import scheduler started"); 25 | List repos = this.repositoryService.list(); 26 | repos.forEach(this.repositoryManager::importIssues); 27 | log.info("Import scheduler finished"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/RepositoryController.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers; 2 | 3 | import com.huseyin.youcontribute.controllers.requests.CreateRepositoryRequest; 4 | import com.huseyin.youcontribute.controllers.resources.RepositoryResource; 5 | import com.huseyin.youcontribute.service.RepositoryService; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | @RestController 12 | @RequestMapping("/repositories") 13 | public class RepositoryController { 14 | 15 | private final RepositoryService repositoryService; 16 | 17 | public RepositoryController(RepositoryService repositoryService) { 18 | this.repositoryService = repositoryService; 19 | } 20 | 21 | @PostMapping 22 | @ResponseStatus(HttpStatus.CREATED) 23 | public void create(@RequestBody CreateRepositoryRequest request) { 24 | this.repositoryService.create(request.getOrganization(), request.getRepository()); 25 | } 26 | 27 | @GetMapping 28 | public List list() { 29 | return RepositoryResource.createFor(this.repositoryService.list()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/exceptions/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.exceptions; 2 | 3 | import javax.persistence.EntityNotFoundException; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | import org.springframework.web.bind.annotation.ResponseStatus; 11 | 12 | @ControllerAdvice 13 | public class GlobalExceptionHandler { 14 | 15 | @ExceptionHandler(DuplicatedRepositoryException.class) 16 | @ResponseStatus(HttpStatus.CONFLICT) 17 | @ResponseBody 18 | public ErrorResponse handleDuplicatedRepositoryException(DuplicatedRepositoryException exception, 19 | HttpServletRequest request) { 20 | return ErrorResponse.builder().message(exception.getMessage()).build(); 21 | } 22 | 23 | @ExceptionHandler(EntityNotFoundException.class) 24 | @ResponseStatus(HttpStatus.NOT_FOUND) 25 | @ResponseBody 26 | public ErrorResponse handleEntityNotFoundException(EntityNotFoundException exception, 27 | HttpServletRequest request) { 28 | return ErrorResponse.builder().message(exception.getMessage()).build(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/IssueController.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers; 2 | 3 | import java.util.List; 4 | 5 | import com.huseyin.youcontribute.controllers.requests.CreateRepositoryRequest; 6 | import com.huseyin.youcontribute.controllers.resources.IssueResource; 7 | import com.huseyin.youcontribute.service.IssueService; 8 | import com.huseyin.youcontribute.service.RepositoryService; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | import org.springframework.web.bind.annotation.ResponseStatus; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | @RequestMapping("/issues") 20 | public class IssueController { 21 | 22 | private final IssueService issueService; 23 | 24 | public IssueController(IssueService issueService) { 25 | this.issueService = issueService; 26 | } 27 | 28 | @GetMapping 29 | public List list(@RequestParam("repository_id") Integer repositoryId) { 30 | return IssueResource.createFor(this.issueService.list(repositoryId)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.config; 2 | 3 | import java.util.concurrent.Executor; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.scheduling.annotation.AsyncConfigurer; 10 | import org.springframework.scheduling.annotation.EnableAsync; 11 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 12 | 13 | @EnableAsync 14 | @Configuration 15 | @Slf4j 16 | public class AsyncConfig implements AsyncConfigurer { 17 | 18 | @Override 19 | public Executor getAsyncExecutor() { 20 | ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); 21 | taskExecutor.setThreadNamePrefix("import-issues-executor-"); 22 | taskExecutor.setCorePoolSize(2); 23 | taskExecutor.setMaxPoolSize(5); 24 | taskExecutor.setQueueCapacity(10000); 25 | taskExecutor.initialize(); 26 | return taskExecutor; 27 | } 28 | 29 | @Override 30 | public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { 31 | return (ex, method, params) -> log.error( 32 | String.format("Unexpected error occurred invoking async method: %s", method), ex); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/service/IssueService.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.service; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.EntityNotFoundException; 6 | import javax.transaction.Transactional; 7 | 8 | import com.huseyin.youcontribute.models.Issue; 9 | import com.huseyin.youcontribute.models.Repository; 10 | import com.huseyin.youcontribute.repositories.IssueRepository; 11 | import lombok.AllArgsConstructor; 12 | import org.springframework.stereotype.Service; 13 | 14 | @Service 15 | @AllArgsConstructor 16 | public class IssueService { 17 | 18 | private final IssueRepository issueRepository; 19 | 20 | private final RepositoryService repositoryService; 21 | 22 | @Transactional 23 | public void saveAll(List issues) { 24 | issues.forEach(issue -> { 25 | if (this.issueRepository.findByGithubIssueId(issue.getGithubIssueId()).isEmpty()) { 26 | this.issueRepository.save(issue); 27 | } 28 | }); 29 | } 30 | 31 | public List list(Integer repositoryId) { 32 | Repository repository = this.repositoryService.findById(repositoryId); 33 | return this.issueRepository.findByRepository(repository); 34 | } 35 | 36 | public Issue findRandomIssue() { 37 | return this.issueRepository.findRandomIssue() 38 | .orElseThrow(() -> new EntityNotFoundException("No issues found.")); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /k8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: youcontribute 5 | labels: 6 | app: youcontribute 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: youcontribute 12 | template: 13 | metadata: 14 | labels: 15 | app: youcontribute 16 | spec: 17 | containers: 18 | - name: youcontribute 19 | image: huseyinbabal/youcontribute:v1 20 | ports: 21 | - containerPort: 8080 22 | env: 23 | - name: SPRING_DATASOURCE_URL 24 | value: jdbc:mysql://youcontribute-mysql.default:3306/youcontribute 25 | - name: SPRING_DATASOURCE_USERNAME 26 | value: root 27 | - name: SPRING_DATASOURCE_PASSWORD 28 | valueFrom: 29 | secretKeyRef: 30 | key: db_password 31 | name: youcontribute 32 | - name: GITHUB_API_TOKEN 33 | valueFrom: 34 | secretKeyRef: 35 | key: github_api_token 36 | name: youcontribute 37 | - name: ONE_SIGNAL_API_AUTH_KEY 38 | valueFrom: 39 | secretKeyRef: 40 | key: one_signal_api_auth_key 41 | name: youcontribute 42 | - name: ONE_SIGNAL_APP_ID 43 | valueFrom: 44 | secretKeyRef: 45 | key: one_signal_app_id 46 | name: youcontribute 47 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/models/IssueChallenge.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.models; 2 | 3 | import java.util.Date; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.EnumType; 7 | import javax.persistence.Enumerated; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.OneToOne; 12 | import javax.persistence.Temporal; 13 | import javax.persistence.TemporalType; 14 | 15 | import com.fasterxml.jackson.annotation.JsonManagedReference; 16 | import lombok.AllArgsConstructor; 17 | import lombok.Builder; 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | import org.hibernate.annotations.CreationTimestamp; 21 | import org.hibernate.annotations.GenericGenerator; 22 | import org.hibernate.annotations.UpdateTimestamp; 23 | 24 | @Entity 25 | @Data 26 | @AllArgsConstructor 27 | @NoArgsConstructor 28 | @Builder 29 | public class IssueChallenge { 30 | 31 | @Id 32 | @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") 33 | @GenericGenerator(strategy = "native", name = "native") 34 | private Integer id; 35 | 36 | @OneToOne 37 | @JsonManagedReference 38 | private Issue issue; 39 | 40 | @Enumerated(EnumType.STRING) 41 | private IssueChallengeStatus status; 42 | 43 | @CreationTimestamp 44 | @Temporal(TemporalType.TIMESTAMP) 45 | private Date createdAt; 46 | 47 | @UpdateTimestamp 48 | @Temporal(TemporalType.TIMESTAMP) 49 | private Date updatedAt; 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/models/Repository.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.models; 2 | 3 | import java.util.Date; 4 | import java.util.Set; 5 | 6 | import com.fasterxml.jackson.annotation.JsonBackReference; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import org.hibernate.annotations.CreationTimestamp; 12 | import org.hibernate.annotations.GenericGenerator; 13 | import org.hibernate.annotations.UpdateTimestamp; 14 | 15 | import javax.persistence.CascadeType; 16 | import javax.persistence.Entity; 17 | import javax.persistence.FetchType; 18 | import javax.persistence.GeneratedValue; 19 | import javax.persistence.GenerationType; 20 | import javax.persistence.Id; 21 | import javax.persistence.OneToMany; 22 | import javax.persistence.Temporal; 23 | import javax.persistence.TemporalType; 24 | 25 | @Entity 26 | @Data 27 | @AllArgsConstructor 28 | @NoArgsConstructor 29 | @Builder 30 | public class Repository { 31 | 32 | @Id 33 | @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") 34 | @GenericGenerator(strategy = "native", name = "native") 35 | private Integer id; 36 | 37 | private String organization; 38 | 39 | private String repository; 40 | 41 | @CreationTimestamp 42 | @Temporal(TemporalType.TIMESTAMP) 43 | private Date createdAt; 44 | 45 | @UpdateTimestamp 46 | @Temporal(TemporalType.TIMESTAMP) 47 | private Date updatedAt; 48 | 49 | @OneToMany(mappedBy = "repository", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) 50 | @JsonBackReference 51 | private Set issues; 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/schedulers/ChallengeIssuesScheduler.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.schedulers; 2 | 3 | import com.huseyin.youcontribute.clients.OneSignalClient; 4 | import com.huseyin.youcontribute.models.Issue; 5 | import com.huseyin.youcontribute.models.IssueChallenge; 6 | import com.huseyin.youcontribute.service.IssueChallengeService; 7 | import com.huseyin.youcontribute.service.IssueService; 8 | import lombok.AllArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | @Slf4j 15 | @AllArgsConstructor 16 | public class ChallengeIssuesScheduler { 17 | 18 | private final IssueService issueService; 19 | 20 | private final IssueChallengeService issueChallengeService; 21 | 22 | private final OneSignalClient oneSignalClient; 23 | 24 | @Scheduled(fixedRateString = "${application.challenge-frequency}") 25 | public void challengeIssuesScheduler() { 26 | log.info("Challenge issue scheduler started"); 27 | if (this.issueChallengeService.hasOngoingChallenge()) { 28 | log.warn("There is already an ongoing challenge, skipping..."); 29 | return; 30 | } 31 | Issue randomIssue = this.issueService.findRandomIssue(); 32 | log.info("Found a random issue {}", randomIssue.getId()); 33 | IssueChallenge issueChallenge = issueChallengeService.create(randomIssue); 34 | oneSignalClient.sendNotification(issueChallenge.getId(), randomIssue.getTitle()); 35 | log.info("Challenge issue scheduler finished"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/resources/IssueChallengeResource.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers.resources; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import com.huseyin.youcontribute.models.Issue; 8 | import com.huseyin.youcontribute.models.IssueChallenge; 9 | import com.huseyin.youcontribute.models.IssueChallengeStatus; 10 | import lombok.Builder; 11 | import lombok.Data; 12 | 13 | @Data 14 | @Builder 15 | public class IssueChallengeResource { 16 | 17 | private Integer id; 18 | 19 | private Integer issueId; 20 | 21 | private Integer repositoryId; 22 | 23 | private String repositoryTitle; 24 | 25 | private String issueTitle; 26 | 27 | private Date creationDate; 28 | 29 | private IssueChallengeStatus status; 30 | 31 | public static IssueChallengeResource createFor(IssueChallenge issueChallenge) { 32 | Issue issue = issueChallenge.getIssue(); 33 | return IssueChallengeResource.builder() 34 | .id(issueChallenge.getId()) 35 | .issueId(issue.getId()) 36 | .repositoryId(issue.getRepository().getId()) 37 | .issueTitle(issue.getTitle()) 38 | .repositoryTitle(issue.getRepository().getRepository()) 39 | .creationDate(issueChallenge.getCreatedAt()) 40 | .status(issueChallenge.getStatus()) 41 | .build(); 42 | } 43 | 44 | public static List createFor(List issueChallenges) { 45 | return issueChallenges.stream().map(IssueChallengeResource::createFor).collect(Collectors.toList()); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/models/Issue.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.models; 2 | 3 | import javax.persistence.CascadeType; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.FetchType; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.ManyToOne; 12 | import javax.persistence.OneToMany; 13 | import javax.persistence.OneToOne; 14 | 15 | import com.fasterxml.jackson.annotation.JsonBackReference; 16 | import com.fasterxml.jackson.annotation.JsonManagedReference; 17 | import lombok.AllArgsConstructor; 18 | import lombok.Builder; 19 | import lombok.Data; 20 | import lombok.NoArgsConstructor; 21 | import org.hibernate.annotations.GenericGenerator; 22 | 23 | @Entity 24 | @Data 25 | @AllArgsConstructor 26 | @NoArgsConstructor 27 | @Builder 28 | public class Issue { 29 | 30 | @Id 31 | @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") 32 | @GenericGenerator(strategy = "native", name = "native") 33 | private Integer id; 34 | 35 | private Long githubIssueId; 36 | 37 | private Integer githubIssueNumber; 38 | 39 | private String title; 40 | 41 | @Column(columnDefinition = "text") 42 | private String body; 43 | 44 | private String url; 45 | 46 | @ManyToOne 47 | @JoinColumn 48 | @JsonManagedReference 49 | private Repository repository; 50 | 51 | @OneToOne(mappedBy = "issue", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) 52 | @JsonBackReference 53 | private IssueChallenge issueChallenge; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/service/IssueChallengeService.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.service; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.EntityNotFoundException; 6 | import javax.transaction.Transactional; 7 | 8 | import com.huseyin.youcontribute.models.Issue; 9 | import com.huseyin.youcontribute.models.IssueChallenge; 10 | import com.huseyin.youcontribute.models.IssueChallengeStatus; 11 | import com.huseyin.youcontribute.repositories.IssueChallengeRepository; 12 | import lombok.AllArgsConstructor; 13 | import org.springframework.stereotype.Service; 14 | 15 | @Service 16 | @AllArgsConstructor 17 | public class IssueChallengeService { 18 | 19 | private final IssueChallengeRepository issueChallengeRepository; 20 | 21 | @Transactional 22 | public IssueChallenge create(Issue issue) { 23 | IssueChallenge challenge = IssueChallenge.builder().issue(issue).status(IssueChallengeStatus.PENDING) 24 | .build(); 25 | return this.issueChallengeRepository.save(challenge); 26 | } 27 | 28 | public Boolean hasOngoingChallenge() { 29 | return this.issueChallengeRepository.findByStatusIn(IssueChallengeStatus.ongoingStatuses()).isPresent(); 30 | } 31 | 32 | public void updateStatus(Integer id, IssueChallengeStatus status) { 33 | IssueChallenge issueChallenge = this.issueChallengeRepository.findById(id) 34 | .orElseThrow(() -> new EntityNotFoundException("Issue Challenge " + id + " not found")); 35 | issueChallenge.setStatus(status); 36 | this.issueChallengeRepository.save(issueChallenge); 37 | } 38 | 39 | public List list() { 40 | return this.issueChallengeRepository.findAll(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/service/RepositoryService.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.service; 2 | 3 | import com.huseyin.youcontribute.exceptions.DuplicatedRepositoryException; 4 | import com.huseyin.youcontribute.models.Repository; 5 | import com.huseyin.youcontribute.repositories.RepositoryRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.persistence.EntityNotFoundException; 9 | import javax.transaction.Transactional; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | public class RepositoryService { 15 | 16 | private final RepositoryRepository repositoryRepository; 17 | 18 | public RepositoryService(RepositoryRepository repositoryRepository) { 19 | this.repositoryRepository = repositoryRepository; 20 | } 21 | 22 | @Transactional 23 | public void create(String organization, String repository) { 24 | this.validate(organization, repository); 25 | Repository r = Repository.builder().organization(organization).repository(repository).build(); 26 | this.repositoryRepository.save(r); 27 | } 28 | 29 | public List list() { 30 | return repositoryRepository.findAll(); 31 | } 32 | 33 | private void validate(String organization, String repository) { 34 | this.repositoryRepository.findByOrganizationAndRepository(organization, repository) 35 | .ifPresent((r) -> { 36 | throw new DuplicatedRepositoryException(organization, repository); 37 | }); 38 | } 39 | 40 | public Repository findById(Integer repositoryId) { 41 | return this.repositoryRepository.findById(repositoryId).orElseThrow( 42 | () -> new EntityNotFoundException(String.format("Repository: %d is not found", repositoryId))); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/controllers/IssueChallengeController.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.controllers; 2 | 3 | import java.util.List; 4 | 5 | import com.huseyin.youcontribute.controllers.requests.UpdateChallengeStatusRequest; 6 | import com.huseyin.youcontribute.controllers.resources.IssueChallengeResource; 7 | import com.huseyin.youcontribute.controllers.resources.IssueResource; 8 | import com.huseyin.youcontribute.models.IssueChallenge; 9 | import com.huseyin.youcontribute.service.IssueChallengeService; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PatchMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.ResponseBody; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | @RestController 21 | @RequestMapping("/challenges") 22 | public class IssueChallengeController { 23 | 24 | private final IssueChallengeService issueChallengeService; 25 | 26 | public IssueChallengeController( 27 | IssueChallengeService issueChallengeService) { 28 | this.issueChallengeService = issueChallengeService; 29 | } 30 | 31 | @PatchMapping("/{id}") 32 | public void updateStatus(@PathVariable("id") Integer id, @RequestBody UpdateChallengeStatusRequest request) { 33 | this.issueChallengeService.updateStatus(id, request.getStatus()); 34 | } 35 | 36 | @GetMapping 37 | public List list() { 38 | return IssueChallengeResource.createFor(this.issueChallengeService.list()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/clients/OneSignalClient.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.clients; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | 7 | import com.currencyfair.onesignal.OneSignal; 8 | import com.currencyfair.onesignal.model.notification.Button; 9 | import com.currencyfair.onesignal.model.notification.NotificationRequest; 10 | import com.huseyin.youcontribute.config.ApplicationProperties; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | public class OneSignalClient { 15 | 16 | private final ApplicationProperties applicationProperties; 17 | 18 | public OneSignalClient(ApplicationProperties applicationProperties) { 19 | this.applicationProperties = applicationProperties; 20 | } 21 | 22 | public void sendNotification(Integer challengeId, String issueTitle) { 23 | NotificationRequest request = new NotificationRequest(); 24 | ApplicationProperties.OneSignalProperties oneSignal = this.applicationProperties.getOneSignal(); 25 | request.setTemplateId(oneSignal.getNewChallengeTemplateId()); 26 | request.setAppId(oneSignal.getAppId()); 27 | request.setAnyWeb(true); 28 | request.setContents(new HashMap<>() {{ 29 | put("en", String.format("Would you like to solve following challenge?\n%s", issueTitle)); 30 | }}); 31 | Button acceptButton = new Button(); 32 | acceptButton.setId("accept"); 33 | acceptButton.setText("Accept"); 34 | acceptButton.setUrl(String.format("http://localhost:4200/challenge/%d/accept", challengeId)); 35 | Button rejectButton = new Button(); 36 | rejectButton.setId("reject"); 37 | rejectButton.setText("Reject"); 38 | rejectButton.setUrl(String.format("http://localhost:4200/challenge/%d/reject", challengeId)); 39 | request.setWebButtons(Arrays.asList(acceptButton, rejectButton)); 40 | request.setIncludedSegments(new ArrayList<>(){{add("Subscribed Users");}}); 41 | OneSignal.createNotification(oneSignal.getApiAuthKey(), request); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/huseyin/youcontribute/clients/OneSignalClientTest.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.clients; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | 8 | import com.currencyfair.onesignal.OneSignal; 9 | import com.currencyfair.onesignal.model.notification.Button; 10 | import com.currencyfair.onesignal.model.notification.NotificationRequest; 11 | import org.junit.jupiter.api.Test; 12 | import org.testcontainers.shaded.com.google.common.collect.ImmutableList; 13 | 14 | import static org.junit.jupiter.api.Assertions.*; 15 | 16 | class OneSignalClientTest { 17 | 18 | @Test 19 | public void it_should_send_notification() { 20 | //given 21 | 22 | NotificationRequest request = new NotificationRequest(); 23 | request.setTemplateId("4f90f941-cdd2-43a4-b932-6ecff434b355"); 24 | request.setAppId("651f0ad7-99cb-4370-a196-38a21a6994b7"); 25 | request.setAnyWeb(true); 26 | request.setContents(new HashMap<>() {{ 27 | put("base_url", "http://localhost:4200"); 28 | put("issue_title", "Production kubernetes cluster is deleted accidentally!"); 29 | put("challenge_id", "123123"); 30 | }}); 31 | request.setContents(new HashMap<>() {{ 32 | put("en", "fasdfasfasfasdfsafsadf"); 33 | }}); 34 | Button acceptButton = new Button(); 35 | acceptButton.setId("accept"); 36 | acceptButton.setText("Accept"); 37 | acceptButton.setUrl("http://localhost:4200/challenges/5/accept"); 38 | Button rejectButton = new Button(); 39 | rejectButton.setId("reject"); 40 | rejectButton.setText("Reject"); 41 | rejectButton.setUrl("http://localhost:4200/challenges/5/reject"); 42 | request.setWebButtons(Arrays.asList(acceptButton, rejectButton)); 43 | request.setIncludedSegments(new ArrayList<>(){{add("Subscribed Users");}}); 44 | OneSignal.createNotification("ZWRmMzAyYmEtZGU2NC00NjliLTg5MjMtOTM2MTA2NDg2OTk0", request); 45 | //when 46 | 47 | //then 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/schedulers/TrackChallengesScheduler.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.schedulers; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.huseyin.youcontribute.clients.OneSignalClient; 6 | import com.huseyin.youcontribute.models.Issue; 7 | import com.huseyin.youcontribute.models.IssueChallenge; 8 | import com.huseyin.youcontribute.models.IssueChallengeStatus; 9 | import com.huseyin.youcontribute.models.Repository; 10 | import com.huseyin.youcontribute.service.GithubClient; 11 | import com.huseyin.youcontribute.service.IssueChallengeService; 12 | import com.huseyin.youcontribute.service.IssueService; 13 | import lombok.AllArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.scheduling.annotation.Scheduled; 16 | import org.springframework.stereotype.Component; 17 | 18 | @Component 19 | @Slf4j 20 | @AllArgsConstructor 21 | public class TrackChallengesScheduler { 22 | 23 | private final IssueChallengeService issueChallengeService; 24 | 25 | private final GithubClient githubClient; 26 | 27 | @Scheduled(fixedRateString = "${application.challenge-tracking-frequency}") 28 | public void challengeIssuesScheduler() { 29 | log.info("Track challenge scheduler started"); 30 | this.issueChallengeService.list() 31 | .stream() 32 | .filter(issueChallenge -> IssueChallengeStatus.ACCEPTED.equals(issueChallenge.getStatus())) 33 | .forEach(issueChallenge -> { 34 | Repository repository = issueChallenge.getIssue().getRepository(); 35 | Arrays.stream(this.githubClient 36 | .listPullRequests(repository.getOrganization(), repository.getRepository())) 37 | .filter(pull -> "huseyinbabal".equals(pull.getUser().getLogin()) && pull.getBody().contains(String.format("Fixes #%d", issueChallenge.getIssue().getGithubIssueNumber())) && "closed".equals(pull.getState())) 38 | .findFirst() 39 | .ifPresent(pullResponse -> { 40 | this.issueChallengeService.updateStatus(issueChallenge.getId(), IssueChallengeStatus.COMPLETED); 41 | }); 42 | }); 43 | log.info("Track challenge scheduler finished"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/managers/RepositoryManager.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.managers; 2 | 3 | import java.time.Instant; 4 | import java.time.LocalDate; 5 | import java.time.ZoneOffset; 6 | import java.time.temporal.ChronoUnit; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Objects; 10 | import java.util.stream.Collectors; 11 | 12 | import com.huseyin.youcontribute.config.ApplicationProperties; 13 | import com.huseyin.youcontribute.models.Issue; 14 | import com.huseyin.youcontribute.models.Repository; 15 | import com.huseyin.youcontribute.service.GithubClient; 16 | import com.huseyin.youcontribute.service.IssueService; 17 | import com.huseyin.youcontribute.service.RepositoryService; 18 | import com.huseyin.youcontribute.service.models.GithubIssueResponse; 19 | import lombok.AllArgsConstructor; 20 | import org.springframework.scheduling.annotation.Async; 21 | import org.springframework.stereotype.Service; 22 | 23 | @Service 24 | @AllArgsConstructor 25 | public class RepositoryManager { 26 | 27 | private final RepositoryService repositoryService; 28 | 29 | private final IssueService issueService; 30 | 31 | private final GithubClient githubClient; 32 | 33 | private final ApplicationProperties applicationProperties; 34 | 35 | public void importRepository(String organization, String repository) { 36 | this.repositoryService.create(organization, repository); 37 | } 38 | 39 | @Async 40 | public void importIssues(Repository repository) { 41 | int schedulerFrequencyInMinutes = applicationProperties.getImportFrequency() / 60000; 42 | LocalDate since = LocalDate.ofInstant(Instant.now().minus(schedulerFrequencyInMinutes, ChronoUnit.MINUTES), 43 | ZoneOffset.UTC); 44 | GithubIssueResponse[] githubIssueResponses = this.githubClient.listIssues( 45 | repository.getOrganization(), repository.getRepository(), since); 46 | List issues = Arrays.stream(githubIssueResponses).filter(githubIssue -> Objects.isNull(githubIssue.getPullRequest())).map(githubIssue -> Issue.builder().repository(repository).githubIssueNumber(githubIssue.getNumber()).githubIssueId(githubIssue.getId()).url(githubIssue.getHtmlUrl()).title(githubIssue.getTitle()).body(githubIssue.getBody()).build()).collect( 47 | Collectors.toList()); 48 | this.issueService.saveAll(issues); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/service/GithubClient.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.service; 2 | 3 | import java.time.LocalDate; 4 | import java.util.Date; 5 | 6 | import com.huseyin.youcontribute.config.GithubProperties; 7 | import com.huseyin.youcontribute.service.models.GithubIssueResponse; 8 | import com.huseyin.youcontribute.service.models.GithubPullResponse; 9 | import org.springframework.http.HttpEntity; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.web.client.RestTemplate; 15 | 16 | @Service 17 | public class GithubClient { 18 | 19 | private final RestTemplate restTemplate; 20 | 21 | private final GithubProperties githubProperties; 22 | 23 | public GithubClient(RestTemplate restTemplate, 24 | GithubProperties githubProperties) { 25 | this.restTemplate = restTemplate; 26 | this.githubProperties = githubProperties; 27 | } 28 | 29 | public GithubIssueResponse[] listIssues(String owner, String repository, LocalDate since) { 30 | String issuesUrl = String.format("%s/repos/%s/%s/issues?since=%s", this.githubProperties.getApiUrl(), 31 | owner, repository, since.toString()); 32 | HttpHeaders headers = new HttpHeaders(); 33 | headers.add("Authorization", "token " + this.githubProperties.getToken()); 34 | HttpEntity request = new HttpEntity(headers); 35 | ResponseEntity response = this.restTemplate.exchange(issuesUrl, HttpMethod.GET, 36 | request, GithubIssueResponse[].class); 37 | return response.getBody(); 38 | } 39 | 40 | public GithubPullResponse[] listPullRequests(String owner, String repository) { 41 | String pullRequestsUrl = String.format("%s/repos/%s/%s/pulls?state=closed", this.githubProperties.getApiUrl(), 42 | owner, repository); 43 | HttpHeaders headers = new HttpHeaders(); 44 | headers.add("Authorization", "token " + this.githubProperties.getToken()); 45 | HttpEntity request = new HttpEntity(headers); 46 | ResponseEntity response = this.restTemplate.exchange(pullRequestsUrl, HttpMethod.GET, 47 | request, GithubPullResponse[].class); 48 | return response.getBody(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/huseyin/youcontribute/service/models/GithubIssueResponse.java: -------------------------------------------------------------------------------- 1 | package com.huseyin.youcontribute.service.models; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 8 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 9 | import lombok.Data; 10 | 11 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 12 | @Data 13 | public class GithubIssueResponse { 14 | public String url; 15 | public String repositoryUrl; 16 | public String labelsUrl; 17 | public String commentsUrl; 18 | public String eventsUrl; 19 | public String htmlUrl; 20 | public long id; 21 | public String nodeId; 22 | public int number; 23 | public String title; 24 | public User user; 25 | public List