├── Duga ├── gradle.properties ├── settings.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.jar ├── upload-lambda.bat ├── duga-aws │ ├── src │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── net │ │ │ │ └── zomis │ │ │ │ └── duga │ │ │ │ ├── tasks │ │ │ │ ├── DugaTask.kt │ │ │ │ ├── MessageTask.kt │ │ │ │ ├── CommentScanTask.kt │ │ │ │ ├── UnansweredTask.kt │ │ │ │ └── TaskLambda.kt │ │ │ │ ├── hooks │ │ │ │ ├── DugaWebhook.kt │ │ │ │ ├── GitHubHook.kt │ │ │ │ ├── SplunkHook.kt │ │ │ │ └── HookLambda.kt │ │ │ │ ├── utils │ │ │ │ ├── DugaStats.kt │ │ │ │ ├── StackExchangeAPI.kt │ │ │ │ └── GitHubAPI.kt │ │ │ │ └── aws │ │ │ │ ├── DugaState.kt │ │ │ │ └── Duga.kt │ │ └── test │ │ │ └── kotlin │ │ │ └── net │ │ │ └── zomis │ │ │ └── duga │ │ │ └── utils │ │ │ └── HookTest.kt │ └── build.gradle.kts ├── build.gradle.kts └── gradlew.bat ├── settings.gradle ├── duga-core ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── zomis │ │ │ └── duga │ │ │ ├── chat │ │ │ ├── listen │ │ │ │ ├── ChatMessageRetriever.java │ │ │ │ ├── ListenRoot.java │ │ │ │ ├── ListenTask.java │ │ │ │ └── StackExchangeFetch.java │ │ │ ├── events │ │ │ │ ├── DugaStopEvent.java │ │ │ │ ├── DugaStartedEvent.java │ │ │ │ ├── DugaLoginEvent.java │ │ │ │ ├── DugaEvent.java │ │ │ │ └── DugaPrepostEvent.java │ │ │ ├── LoginFunction.java │ │ │ ├── ProbablyNotLoggedInException.java │ │ │ ├── ChatThrottleException.java │ │ │ ├── state │ │ │ │ ├── BotState.java │ │ │ │ └── BotCookie.java │ │ │ ├── ChatMessageResponse.java │ │ │ ├── ChatBot.java │ │ │ ├── BotRoom.java │ │ │ ├── BotConfiguration.java │ │ │ ├── TestBot.java │ │ │ ├── ChatMessage.java │ │ │ └── RemoteBot.java │ │ │ └── DugaBot2.java │ └── test │ │ └── java │ │ └── net │ │ └── zomis │ │ └── duga │ │ └── chat │ │ └── listen │ │ └── ChatMessageIncomingTest.java ├── build.gradle └── gradlew.bat ├── duga-ktor ├── settings.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── resources │ ├── application.conf │ └── log4j2.xml ├── Dockerfile ├── src │ ├── Structure.kt │ ├── chat │ │ ├── DugaBot.kt │ │ ├── DugaClient.kt │ │ ├── StackExchangeLogin.kt │ │ └── listener │ │ │ └── ChatListener.kt │ ├── utils │ │ ├── stackexchange │ │ │ ├── QuestionScanTask.kt │ │ │ └── StackExchangeApi.kt │ │ └── github │ │ │ └── GitHubApi.kt │ ├── DugaTasks.kt │ ├── server │ │ └── webhooks │ │ │ ├── SplunkWebhook.kt │ │ │ ├── AppVeyorWebhook.kt │ │ │ ├── GitHubWebhook.kt │ │ │ └── StatsWebhook.kt │ └── DugaMain.kt ├── test │ └── ApplicationTest.kt ├── gradlew.bat └── build.gradle.kts ├── gradle.properties ├── runduga.bat ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── grails-app ├── assets │ ├── images │ │ ├── favicon.ico │ │ ├── spinner.gif │ │ ├── grails_logo.png │ │ ├── skin │ │ │ ├── house.png │ │ │ ├── shadow.jpg │ │ │ ├── database_add.png │ │ │ ├── exclamation.png │ │ │ ├── information.png │ │ │ ├── sorted_asc.gif │ │ │ ├── sorted_desc.gif │ │ │ ├── database_edit.png │ │ │ ├── database_save.png │ │ │ ├── database_table.png │ │ │ └── database_delete.png │ │ ├── springsource.png │ │ ├── apple-touch-icon.png │ │ └── apple-touch-icon-retina.png │ ├── stylesheets │ │ ├── application.css │ │ ├── mobile.css │ │ └── errors.css │ └── javascripts │ │ └── application.js ├── domain │ └── net │ │ └── zomis │ │ └── duga │ │ ├── DugaConfig.groovy │ │ ├── Authority.groovy │ │ ├── TaskData.groovy │ │ ├── Followed.groovy │ │ ├── DailyInfo.groovy │ │ └── UserAuthority.groovy ├── services │ └── net │ │ └── zomis │ │ └── duga │ │ ├── StackAPI.groovy │ │ ├── DugaMachineLearning.groovy │ │ ├── DugaFileConfig.groovy │ │ ├── StackExchangeAPI.groovy │ │ ├── GithubBean.groovy │ │ ├── DugaChatListener.groovy │ │ ├── GormUserDetailsService.groovy │ │ ├── DugaGit.groovy │ │ └── DugaBotService.groovy ├── controllers │ ├── net │ │ └── zomis │ │ │ └── duga │ │ │ ├── AdminController.groovy │ │ │ ├── HomeController.groovy │ │ │ ├── ManualStatsController.groovy │ │ │ ├── SplunkController.groovy │ │ │ ├── GithubHookController.groovy │ │ │ ├── BitbucketController.groovy │ │ │ ├── AppveyorHookController.groovy │ │ │ └── RegistrationController.groovy │ └── UrlMappings.groovy ├── views │ ├── notFound.gsp │ ├── layouts │ │ └── main.gsp │ ├── error.gsp │ ├── user │ │ ├── index.gsp │ │ ├── show.gsp │ │ ├── create.gsp │ │ ├── signup.gsp │ │ └── edit.gsp │ ├── taskData │ │ ├── index.gsp │ │ ├── show.gsp │ │ ├── create.gsp │ │ └── edit.gsp │ ├── userAuthority │ │ ├── index.gsp │ │ ├── show.gsp │ │ ├── create.gsp │ │ └── edit.gsp │ └── registration │ │ ├── saved.gsp │ │ └── index.gsp ├── init │ ├── net │ │ └── zomis │ │ │ └── duga │ │ │ ├── Application.groovy │ │ │ └── SecurityConfiguration.groovy │ └── BootStrap.groovy ├── conf │ ├── spring │ │ └── resources.groovy │ ├── logback.groovy │ └── application.groovy └── i18n │ ├── messages_fr.properties │ ├── messages_zh_CN.properties │ ├── messages_ru.properties │ ├── messages_ja.properties │ └── messages_pt_PT.properties ├── docker ├── docker-compose.yml ├── README.md └── Dockerfile ├── src ├── main │ ├── resources │ │ ├── duga_example.groovy │ │ └── typecheck-extension.groovy │ └── groovy │ │ └── net │ │ └── zomis │ │ └── duga │ │ ├── tasks │ │ ├── MessageTask.groovy │ │ ├── UnansweredTask.groovy │ │ ├── ChatScrape.java │ │ └── qscan │ │ │ └── QuestionScanTask.groovy │ │ ├── github │ │ └── GithubEventFilter.groovy │ │ └── model │ │ └── GrailsUser.java ├── test │ └── groovy │ │ └── net │ │ └── zomis │ │ └── duga │ │ └── tasks │ │ └── qscan │ │ └── AnswerInvalidationCheckTest.groovy └── integration-test │ └── groovy │ └── net │ └── zomis │ └── duga │ ├── tasks │ └── qscan │ │ └── StackMockAPI.groovy │ └── github │ └── BitbucketTest.groovy ├── external_example.groovy ├── README.md ├── Jenkinsfile ├── gradlew.bat └── .gitignore /Duga/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'duga-core' 2 | 3 | -------------------------------------------------------------------------------- /duga-core/gradle.properties: -------------------------------------------------------------------------------- 1 | gradleWrapperVersion=2.3 2 | -------------------------------------------------------------------------------- /duga-ktor/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ktor-demo" 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | grailsVersion=3.0.2 2 | gradleWrapperVersion=2.3 3 | -------------------------------------------------------------------------------- /Duga/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'duga' 2 | include 'duga-aws' 3 | 4 | -------------------------------------------------------------------------------- /runduga.bat: -------------------------------------------------------------------------------- 1 | gradlew duga-core:install -Dgrails.server.port=4242 -Dgrails.env=production run 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /grails-app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/favicon.ico -------------------------------------------------------------------------------- /grails-app/assets/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/spinner.gif -------------------------------------------------------------------------------- /Duga/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/Duga/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /grails-app/assets/images/grails_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/grails_logo.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/house.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/shadow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/shadow.jpg -------------------------------------------------------------------------------- /grails-app/assets/images/springsource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/springsource.png -------------------------------------------------------------------------------- /duga-core/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/duga-core/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /duga-ktor/gradle.properties: -------------------------------------------------------------------------------- 1 | logback_version=1.2.1 2 | ktor_version=1.6.7 3 | kotlin.code.style=official 4 | kotlin_version=1.6.0 5 | -------------------------------------------------------------------------------- /duga-ktor/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/duga-ktor/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /duga-ktor/.gitignore: -------------------------------------------------------------------------------- 1 | /*.secret 2 | /duga.log 3 | 4 | /.gradle 5 | /.idea 6 | /out 7 | /build 8 | *.iml 9 | *.ipr 10 | *.iws 11 | -------------------------------------------------------------------------------- /grails-app/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/database_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/database_add.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/exclamation.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/information.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/sorted_asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/sorted_asc.gif -------------------------------------------------------------------------------- /grails-app/assets/images/skin/sorted_desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/sorted_desc.gif -------------------------------------------------------------------------------- /grails-app/assets/images/skin/database_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/database_edit.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/database_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/database_save.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/database_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/database_table.png -------------------------------------------------------------------------------- /grails-app/assets/images/skin/database_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/skin/database_delete.png -------------------------------------------------------------------------------- /grails-app/assets/images/apple-touch-icon-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zomis/Duga/HEAD/grails-app/assets/images/apple-touch-icon-retina.png -------------------------------------------------------------------------------- /grails-app/domain/net/zomis/duga/DugaConfig.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga; 2 | 3 | class DugaConfig { 4 | 5 | String key 6 | String value 7 | 8 | } 9 | -------------------------------------------------------------------------------- /Duga/upload-lambda.bat: -------------------------------------------------------------------------------- 1 | 2 | gradlew shadowJar 3 | aws lambda update-function-code --function-name duga-post-from-sqs --zip-file fileb://duga-aws/build/libs/duga-aws-1.0-SNAPSHOT-all.jar 4 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/StackAPI.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | interface StackAPI { 4 | def apiCall(String apiCall, String site, String filter) throws IOException; 5 | } 6 | -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/AdminController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | class AdminController { 3 | 4 | def index() { 5 | render 'You are in the admin controller' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/HomeController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | class HomeController { 4 | 5 | def index() { 6 | render "You are in the home controller" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/tasks/DugaTask.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import net.zomis.duga.aws.DugaMessage 4 | 5 | interface DugaTask { 6 | fun perform(): List 7 | } 8 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/hooks/DugaWebhook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.hooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | 5 | interface DugaWebhook { 6 | 7 | fun handle(body: JsonNode): List 8 | 9 | } 10 | -------------------------------------------------------------------------------- /duga-ktor/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/listen/ChatMessageRetriever.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.listen; 2 | 3 | import java.util.List; 4 | 5 | public interface ChatMessageRetriever { 6 | 7 | List fetch(String roomId, int count); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 03 18:59:45 CEST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip 7 | -------------------------------------------------------------------------------- /duga-core/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 03 18:59:45 CEST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip 7 | -------------------------------------------------------------------------------- /duga-ktor/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | port = ${?PORT} 5 | 6 | shutdown.url = "/ktor/application/shutdown" 7 | } 8 | application { 9 | modules = [ net.zomis.duga.unused.ApplicationKt.module ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/events/DugaStopEvent.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.events; 2 | 3 | import net.zomis.duga.chat.ChatBot; 4 | 5 | public class DugaStopEvent extends DugaEvent { 6 | 7 | public DugaStopEvent(ChatBot bot) { 8 | super(bot); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /grails-app/domain/net/zomis/duga/Authority.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | class Authority { 4 | 5 | String authority 6 | 7 | /* static mapping = { 8 | cache true 9 | }*/ 10 | 11 | static constraints = { 12 | authority blank: false, unique: true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/events/DugaStartedEvent.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.events; 2 | 3 | import net.zomis.duga.chat.ChatBot; 4 | 5 | public class DugaStartedEvent extends DugaEvent { 6 | 7 | public DugaStartedEvent(ChatBot bot) { 8 | super(bot); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /grails-app/domain/net/zomis/duga/TaskData.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga; 2 | 3 | class TaskData { 4 | 5 | String cronStr 6 | String taskValue 7 | 8 | TaskData() {} 9 | 10 | TaskData(String cronStr, String taskValue) { 11 | this.cronStr = cronStr 12 | this.taskValue = taskValue 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/LoginFunction.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | import com.gistlabs.mechanize.impl.MechanizeAgent; 4 | 5 | public interface LoginFunction { 6 | 7 | MechanizeAgent constructAgent(BotConfiguration configuration); 8 | String retrieveFKey(MechanizeAgent agent, BotConfiguration configuration); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | # command: php -S 0.0.0.0:8000 -t /code 4 | ports: 5 | - "8000:8080" 6 | links: 7 | - db 8 | volumes: 9 | - ./webapp:/usr/local/tomcat/webapps 10 | - .:/code 11 | db: 12 | image: postgres 13 | environment: 14 | POSTGRES_USER: duga 15 | POSTGRES_PASSWORD: duga 16 | POSTGRES_DB: grails 17 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/ProbablyNotLoggedInException.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | /** 4 | * 5 | * @author Frank van Heeswijk 6 | */ 7 | public class ProbablyNotLoggedInException extends Exception { 8 | private static final long serialVersionUID = 6373838392929383L; 9 | 10 | public ProbablyNotLoggedInException() { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/events/DugaLoginEvent.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.events; 2 | 3 | import net.zomis.duga.chat.ChatBot; 4 | 5 | /** 6 | * Executed after bot has logged in, whether successful or not. 7 | */ 8 | public class DugaLoginEvent extends DugaEvent { 9 | 10 | public DugaLoginEvent(ChatBot bot) { 11 | super(bot); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/events/DugaEvent.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.events; 2 | 3 | import net.zomis.duga.chat.ChatBot; 4 | 5 | public class DugaEvent { 6 | 7 | private final ChatBot bot; 8 | 9 | public DugaEvent(ChatBot bot) { 10 | this.bot = bot; 11 | } 12 | 13 | public ChatBot getBot() { 14 | return bot; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/duga_example.groovy: -------------------------------------------------------------------------------- 1 | botName = 'Greger' 2 | rootUrl = 'https://stackexchange.com' 3 | email = 'your@email.com' 4 | password = 'SE account password' 5 | 6 | stackAPI = 'Stack Exchange API Key' 7 | githubAPI = 'Github API Key' 8 | adminDefaultPass = 'adminpassword' 9 | commandPrefix = '@BotName do ' 10 | 11 | dataSource { 12 | username = 'duga' 13 | password = 'duga' 14 | } 15 | -------------------------------------------------------------------------------- /Duga/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") version "1.3.11" 5 | } 6 | 7 | group = "net.zomis" 8 | version = "1.0-SNAPSHOT" 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | compile(kotlin("stdlib-jdk8")) 16 | } 17 | 18 | tasks.withType { 19 | kotlinOptions.jvmTarget = "1.8" 20 | } -------------------------------------------------------------------------------- /duga-ktor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jre 2 | 3 | EXPOSE 3842 4 | 5 | WORKDIR /duga-bot/ 6 | ADD build/libs/*-all.jar /duga-bot/ 7 | 8 | VOLUME /data/logs/ 9 | 10 | CMD java -jar /duga-bot/ktor-demo-0.0.1-SNAPSHOT-all.jar \ 11 | unanswered \ 12 | answer-invalidation comment-scan \ 13 | weekly-update-reminder \ 14 | hello-world \ 15 | stats-dynamodb \ 16 | duga-poster \ 17 | daily-stats 18 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/hooks/GitHubHook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.hooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import net.zomis.duga.utils.HookString 5 | 6 | class GitHubHook(private val githubEventType: String) : DugaWebhook { 7 | 8 | override fun handle(body: JsonNode): List { 9 | return HookString().postGithub(githubEventType, body) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | How to run Duga on Docker 2 | ========================= 3 | 4 | Clone the duga repository 5 | 6 | Edit `src/main/resources/duga_example.groovy` to set your settings. Leave the dataSource configuration as-is. Also rename this to `duga.groovy` 7 | 8 | Run `./gradlew war` 9 | 10 | Create the directory `docker/webapps` and copy `build/libs/*.war` to `docker/webapps/*.war` 11 | 12 | Inside the `docker` directory, run `docker-compose up` 13 | -------------------------------------------------------------------------------- /grails-app/views/notFound.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Not Found 5 | 6 | 7 | 8 | 9 |
    10 |
  • Error: Page Not Found (404)
  • 11 |
  • Path: ${request.forwardURI}
  • 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/tasks/MessageTask.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import net.zomis.duga.aws.DugaMessage 4 | import java.time.Instant 5 | 6 | class MessageTask(private val room: String, private val message: String) : DugaTask { 7 | 8 | override fun perform(): List { 9 | val msg = message.replace("%time%", Instant.now().toString()) 10 | return listOf(DugaMessage(room, msg)) 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/groovy/net/zomis/duga/tasks/qscan/AnswerInvalidationCheckTest.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks.qscan 2 | 3 | import org.junit.Test 4 | 5 | import static net.zomis.duga.tasks.qscan.AnswerInvalidationCheck.formatDisplayName 6 | 7 | class AnswerInvalidationCheckTest { 8 | @Test 9 | public void format_name_with_apostrophe() { 10 | def displayName = "Mat's Mug" 11 | def expected = "Mat's Mug" 12 | assert expected == formatDisplayName(displayName) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /grails-app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS file within this directory can be referenced here using a relative path. 6 | * 7 | * You're free to add application-wide styles to this file and they'll appear at the top of the 8 | * compiled file, but it's generally better to create a new file per style scope. 9 | * 10 | *= require main 11 | *= require mobile 12 | *= require_self 13 | */ 14 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/ChatThrottleException.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | /** 4 | * 5 | * @author Frank van Heeswijk 6 | */ 7 | public class ChatThrottleException extends Exception { 8 | private static final long serialVersionUID = 304320309380200L; 9 | 10 | private final int throttleTiming; 11 | 12 | public ChatThrottleException(final int throttleTiming) { 13 | this.throttleTiming = throttleTiming; 14 | } 15 | 16 | public int getThrottleTiming() { 17 | return throttleTiming; 18 | } 19 | } -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/tasks/CommentScanTask.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import net.zomis.duga.utils.StackExchangeAPI 5 | 6 | class CommentScanTask { 7 | 8 | // Requires learning 9 | fun fetchComments(site: String, fromDate: Long): JsonNode { 10 | val filter = "!Fcb8.PVyNbcSSIFtmbqhHwtwVw" 11 | return StackExchangeAPI().apiCall("comments?page=1&pagesize=100&fromdate=" + fromDate + 12 | "&order=desc&sort=creation", site, filter) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/state/BotState.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.state; 2 | 3 | import java.util.List; 4 | 5 | public class BotState { 6 | 7 | private String fkey; 8 | private List cookies; 9 | 10 | public void setCookies(List cookies) { 11 | this.cookies = cookies; 12 | } 13 | 14 | public void setFkey(String fkey) { 15 | this.fkey = fkey; 16 | } 17 | 18 | public List getCookies() { 19 | return cookies; 20 | } 21 | 22 | public String getFkey() { 23 | return fkey; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /duga-ktor/src/Structure.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | /* 4 | Server 5 | - Web stuff 6 | 7 | 8 | Client 9 | - Post to stack exchange stuff 10 | - Listen for messages 11 | 12 | StackExchange 13 | Site 14 | - Fetch edits 15 | Chat 16 | 17 | 18 | Utils 19 | - HookStringification 20 | - GitHub: Fetch additions and deletions for a specific commit 21 | - Tasks 22 | - StackExchange 23 | - Detect edits 24 | - Reputation races 25 | - Comments scanning 26 | - GitHub 27 | - Star-races 28 | - Newly-created repositories, or commits to other repositories 29 | - Daily reload, with stats 30 | */ 31 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/hooks/SplunkHook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.hooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | 5 | class SplunkHook : DugaWebhook { 6 | 7 | override fun handle(body: JsonNode): List { 8 | val splunkFields: (JsonNode) -> String = {node -> 9 | node.fields().asSequence().associate { it.key to it.value }.map { "${it.key}: ${it.value.asText()}" }.joinToString(", ") 10 | } 11 | val node = body["result"] 12 | return listOf("@SimonForsberg **Splunk Alert:** ${body["search_name"]} - ${splunkFields(node)}") 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /duga-ktor/src/chat/DugaBot.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat 2 | 3 | import io.ktor.client.* 4 | 5 | class DugaBot( 6 | val httpClient: HttpClient, 7 | private val config: BotConfig, 8 | private val fkeyFunction: suspend (HttpClient, BotConfig) -> String 9 | ) { 10 | 11 | val chatUrl: String get() = config.chatUrl 12 | private var fkey: String? = null 13 | 14 | suspend fun fkey(): String { 15 | if (fkey == null) { 16 | refreshFKey() 17 | } 18 | return this.fkey!! 19 | } 20 | 21 | suspend fun refreshFKey() { 22 | fkey = fkeyFunction(httpClient, config) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/integration-test/groovy/net/zomis/duga/tasks/qscan/StackMockAPI.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks.qscan 2 | 3 | import net.zomis.duga.StackAPI 4 | 5 | class StackMockAPI implements StackAPI { 6 | 7 | Map results = [:] 8 | 9 | StackMockAPI expect(String apiCall, def result) { 10 | results.put(apiCall, result) 11 | this 12 | } 13 | 14 | @Override 15 | def apiCall(String apiCall, String site, String filter) throws IOException { 16 | def result = results.get(apiCall) 17 | assert result : "No mock result defined for $apiCall. Keys are ${results.keySet()}" 18 | result 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/ManualStatsController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import org.grails.web.json.JSONObject 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import org.springframework.beans.factory.annotation.Autowired 7 | 8 | class ManualStatsController { 9 | 10 | private static final Logger logger = LoggerFactory.getLogger(ManualStatsController.class) 11 | 12 | static allowedMethods = [stats:'POST'] 13 | 14 | @Autowired 15 | DynamicStats dynamicStats 16 | 17 | def stats() { 18 | JSONObject json = request.JSON 19 | 20 | def result = dynamicStats.save(json) 21 | render('Current: ' + result) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /grails-app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js. 2 | // 3 | // Any JavaScript file within this directory can be referenced here using a relative path. 4 | // 5 | // You're free to add application-wide JavaScript to this file, but it's generally better 6 | // to create separate JavaScript files as needed. 7 | // 8 | //= require jquery-2.1.3.js 9 | //= require_tree . 10 | //= require_self 11 | 12 | if (typeof jQuery !== 'undefined') { 13 | (function($) { 14 | $('#spinner').ajaxStart(function() { 15 | $(this).fadeIn(); 16 | }).ajaxStop(function() { 17 | $(this).fadeOut(); 18 | }); 19 | })(jQuery); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/typecheck-extension.groovy: -------------------------------------------------------------------------------- 1 | beforeMethodCall { call -> 2 | if (isMethodCallExpression(call)) { 3 | ['clone', 'finalize', ''] 4 | def methodName = call.methodAsString 5 | if (methodName == 'wait' || 6 | methodName == 'clone' || 7 | methodName == 'finalize' || 8 | methodName == 'notify' || 9 | methodName == 'notifyAll') { 10 | addStaticTypeError('Not allowed',call) 11 | handled = true 12 | } 13 | } 14 | } 15 | 16 | methodNotFound { receiver, name, argList, argTypes, call -> 17 | if (receiver.name == 'java.util.Map') { 18 | handled = true 19 | makeDynamic(call, classNodeFor(Map)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /grails-app/domain/net/zomis/duga/Followed.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga; 2 | 3 | class Followed { 4 | 5 | String name 6 | long lastChecked 7 | long lastEventId 8 | String roomIds 9 | Integer followType = 0 10 | String interestingEvents 11 | 12 | boolean isUser() { 13 | return followType == 1 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return "Followed{" + 19 | "name='" + name + '\'' + 20 | ", lastChecked=" + lastChecked + 21 | ", lastEventId=" + lastEventId + 22 | ", roomIds='" + roomIds + '\'' + 23 | ", followType=" + followType + 24 | ", interestingEvents='" + interestingEvents + '\'' + 25 | '}'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/DugaMachineLearning.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.tasks.ProgrammersClassification 4 | import net.zomis.machlearn.text.TextClassification 5 | import org.springframework.beans.factory.InitializingBean 6 | 7 | class DugaMachineLearning implements InitializingBean { 8 | 9 | TextClassification programmers 10 | 11 | @Override 12 | void afterPropertiesSet() throws Exception { 13 | URL trainingData = getClass().getClassLoader() 14 | .getResource("trainingset-programmers-comments.txt"); 15 | String source = trainingData?.text; 16 | String[] lines = source?.split("\n"); 17 | this.programmers = ProgrammersClassification.machineLearning(lines); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/DugaBot2.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga; 2 | 3 | public class DugaBot2 { 4 | 5 | /* 6 | Poll for new chat messages 7 | Add posting filters (Duga issue https://github.com/Zomis/Duga/issues/111 ) 8 | Support all chat domains for the future -- https://github.com/Zomis/Duga/issues/57 9 | 10 | classes in net.zomis.duga.chat 11 | 12 | boolean/void postAsync(message, ResponseCallback) 13 | Response response = postAndWait(message) 14 | 15 | net.zomis.duga.tasks.ChatMessageIncoming 16 | net.zomis.duga.tasks.ListenTask 17 | 18 | What if there are 10 bot instances running with the same username and password? All spamming? 19 | -- in a way, it's your fault if that ever happens. You are in control of the username and password. 20 | 21 | */ 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/test/kotlin/net/zomis/duga/utils/HookTest.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.junit.jupiter.api.Test 5 | 6 | class HookTest { 7 | 8 | val mapper = ObjectMapper() 9 | 10 | @Test 11 | fun test() { 12 | val node = mapper.createObjectNode() 13 | node.put("state", "failure") 14 | node.put("name", "Zomis/test") 15 | node.put("sha", "0123456789abcdef") 16 | node.set("branches", mapper.createArrayNode().add( 17 | mapper.createObjectNode().put("name", "master") 18 | )) 19 | node.put("description", "This commit looks bad") 20 | val result = HookString().postGithub("status", node) 21 | println(result) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/listen/ListenRoot.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.listen; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public class ListenRoot { 8 | 9 | @JsonProperty 10 | private int ms; 11 | @JsonProperty 12 | private long time; 13 | @JsonProperty 14 | private long sync; 15 | 16 | @JsonProperty 17 | private List events; 18 | 19 | public int getMs() { 20 | return ms; 21 | } 22 | 23 | public List getEvents() { 24 | return events; 25 | } 26 | 27 | public long getSync() { 28 | return sync; 29 | } 30 | 31 | public long getTime() { 32 | return time; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /duga-core/src/test/java/net/zomis/duga/chat/listen/ChatMessageIncomingTest.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.listen; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.Test; 5 | 6 | import java.io.IOException; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | public class ChatMessageIncomingTest { 11 | 12 | private static final ObjectMapper mapper = new ObjectMapper(); 13 | 14 | @Test 15 | public void cleanHTML() throws IOException { 16 | ChatMessageIncoming msg = fromString("{ \"content\": \"this is a "quote"\" }"); 17 | assertEquals("this is a \"quote\"", msg.cleanHTML()); 18 | } 19 | 20 | private ChatMessageIncoming fromString(String json) throws IOException { 21 | return mapper.readValue(json, ChatMessageIncoming.class); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /grails-app/controllers/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | class UrlMappings { 2 | 3 | static mappings = { 4 | "/$controller/$action?/$id?(.$format)?"{ 5 | constraints { 6 | // apply constraints here 7 | } 8 | } 9 | "/stats"(controller: "manualStats", action: "stats") 10 | "/splunkWebhook"(controller: "splunk", action: "webhook") 11 | "/hook"(controller: "githubHook", action: "hook") 12 | "/hooks/github/payload"(controller: "githubHook", action: "hook") 13 | "/hooks/bitbucket"(controller: "bitbucket", action: "bitbucket") 14 | "/hooks/appveyor"(controller: "appveyorHook", action: "build") 15 | "/hooks/travis/payload"(controller: "travisHook", action: "build") 16 | "/"(view:"/index") 17 | "500"(view:'/error') 18 | "404"(view:'/notFound') 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/groovy/net/zomis/duga/tasks/MessageTask.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import net.zomis.duga.chat.BotRoom; 4 | 5 | import java.time.Instant; 6 | 7 | import net.zomis.duga.DugaBotService; 8 | 9 | public class MessageTask implements Runnable { 10 | 11 | private final DugaBotService chatBot; 12 | private final BotRoom room; 13 | private final String message; 14 | 15 | public MessageTask(DugaBotService chatBot, String room, String message) { 16 | this.chatBot = chatBot; 17 | this.room = chatBot.room(room); 18 | this.message = message; 19 | } 20 | 21 | @Override 22 | public void run() { 23 | chatBot.postSingle(room, message.replace("%time%", Instant.now().toString())); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "MessageTask [chatBot=" + chatBot + ", room=" + room 29 | + ", message=" + message + "]"; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /grails-app/views/layouts/main.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:layoutTitle default="Grails"/> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /grails-app/domain/net/zomis/duga/DailyInfo.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga; 2 | 3 | class DailyInfo { 4 | 5 | String name 6 | String url 7 | String comment = '' 8 | Integer commits = 0 9 | Integer issuesOpened = 0 10 | Integer issuesClosed = 0 11 | Integer additions = 0 12 | Integer deletions = 0 13 | Integer comments = 0 14 | 15 | void addIssues(int opened, int closed, int comments) { 16 | this.issuesOpened += opened; 17 | this.issuesClosed += closed; 18 | this.comments += comments; 19 | } 20 | 21 | void addCommits(int commits, int additions, int deletions) { 22 | this.commits += commits; 23 | this.additions += additions; 24 | this.deletions += deletions; 25 | } 26 | 27 | void reset() { 28 | this.issuesOpened = 0 29 | this.issuesClosed = 0 30 | this.commits = 0 31 | this.deletions = 0 32 | this.additions = 0 33 | this.comments = 0 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /duga-ktor/src/chat/DugaClient.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.apache.* 5 | import io.ktor.client.features.* 6 | import io.ktor.client.features.compression.* 7 | import io.ktor.client.features.cookies.* 8 | import io.ktor.client.features.logging.* 9 | import io.ktor.client.features.websocket.* 10 | 11 | class DugaClient { 12 | 13 | val client = HttpClient(Apache) { 14 | install(ContentEncoding) { 15 | gzip() 16 | } 17 | install(WebSockets) 18 | install(Logging) { 19 | logger = Logger.DEFAULT 20 | level = LogLevel.BODY 21 | } 22 | install(HttpCookies) { 23 | storage = AcceptAllCookiesStorage() 24 | } 25 | install(HttpRedirect) { 26 | checkHttpMethod = false // See https://github.com/ktorio/ktor/issues/1793#issuecomment-613862691 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /duga-ktor/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | duga.log 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | [%d{ISO8601}] %5p %c [%t] - %m%n 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/state/BotCookie.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.state; 2 | 3 | public class BotCookie { 4 | 5 | private String name; 6 | private String value; 7 | private String domain; 8 | 9 | public static BotCookie create(String name, String value, String domain) { 10 | BotCookie cookie = new BotCookie(); 11 | cookie.name = name; 12 | cookie.value = value; 13 | cookie.domain = domain; 14 | return cookie; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public String getValue() { 26 | return value; 27 | } 28 | 29 | public void setValue(String value) { 30 | this.value = value; 31 | } 32 | 33 | public String getDomain() { 34 | return domain; 35 | } 36 | 37 | public void setDomain(String domain) { 38 | this.domain = domain; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /external_example.groovy: -------------------------------------------------------------------------------- 1 | tasks { 2 | cron('0 * * * * *') { 3 | message('20298', 'Once per minute') 4 | 5 | comments('stackoverflow') { 6 | if (it.toLowerCase().body_markdown.contains('rubberduck')) { 7 | it.post(14929) 8 | } 9 | } 10 | 11 | listen('20298', 20) { 12 | if (it.userId == 98071) { 13 | it.reply('Yes master!') 14 | } 15 | if (it.message == 'lol') { 16 | it.star() 17 | } 18 | } 19 | } 20 | 21 | cron('0 0 * * * *') { 22 | message('20298', 'Once per hour') 23 | 24 | github { 25 | user('Zomis') { 26 | events 'IssuesEvent' 27 | } 28 | repo('Cardshifter/Cardshifter') { 29 | events '*' 30 | } 31 | } 32 | 33 | repdiff('codereview', 23788, 31562) 34 | } 35 | } 36 | 37 | users { 38 | user 'Zomis' password 'topsecret123' { 39 | // stuff... 40 | } 41 | } -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/events/DugaPrepostEvent.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.events; 2 | 3 | import net.zomis.duga.chat.ChatBot; 4 | import net.zomis.duga.chat.ChatMessage; 5 | 6 | public class DugaPrepostEvent extends DugaEvent { 7 | 8 | private final ChatMessage chatMessage; 9 | private boolean performPost; 10 | private String message; 11 | 12 | public DugaPrepostEvent(ChatBot bot, ChatMessage message) { 13 | super(bot); 14 | this.chatMessage = message; 15 | this.message = message.getMessage(); 16 | this.performPost = true; 17 | } 18 | 19 | public ChatMessage getChatMessage() { 20 | return chatMessage; 21 | } 22 | 23 | public String getMessage() { 24 | return message; 25 | } 26 | 27 | public void setMessage(String message) { 28 | this.message = message; 29 | } 30 | 31 | public boolean isPerformPost() { 32 | return performPost; 33 | } 34 | 35 | public void setPerformPost(boolean performPost) { 36 | this.performPost = performPost; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/SplunkController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.chat.BotRoom 4 | import org.grails.web.json.JSONObject 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.beans.factory.annotation.Autowired 8 | 9 | class SplunkController { 10 | 11 | private static final Logger logger = LoggerFactory.getLogger(ManualStatsController.class) 12 | 13 | static allowedMethods = [webhook:'POST'] 14 | 15 | @Autowired 16 | DugaBotService bot 17 | 18 | def webhook() { 19 | JSONObject json = request.JSON 20 | logger.info("Splunk Webhook Triggered: $json") 21 | 22 | String room = params?.roomId ?: "16134" 23 | BotRoom hookParams = bot.room(room) 24 | List strings = stringify(json) 25 | bot.postChat(hookParams.messages(strings)) 26 | render('OK') 27 | } 28 | 29 | static List stringify(JSONObject json) { 30 | return ["@SimonForsberg **Splunk Alert:** ${json.search_name} - ${json.result}".toString()] 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/groovy/net/zomis/duga/github/GithubEventFilter.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.github; 2 | 3 | import java.util.function.Predicate; 4 | import java.util.stream.Collectors; 5 | import java.util.stream.Stream; 6 | 7 | public class GithubEventFilter { 8 | 9 | public static Stream filter(Stream stream, String interestingEvents) { 10 | Set> wantedEvents = Arrays.stream(interestingEvents.split(",")) 11 | .map({str -> predicateMatch(str)}) 12 | .collect(Collectors.toSet()); 13 | 14 | return stream.filter({ev -> wantedEvents.stream().anyMatch({pred -> pred.test(ev)})}); 15 | } 16 | 17 | private static Predicate predicateMatch(String str) { 18 | switch (str) { 19 | case "*": 20 | return {ev -> true} 21 | case "create-tag": 22 | return {ev -> ev.type == 'CreateEvent' && ev.payload.ref_type == 'tag'} 23 | case "create-repository": 24 | return {ev -> ev.type == 'CreateEvent' && ev.payload.ref_type == 'repository'} 25 | default: 26 | return {ev -> ev.type == str} 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/DugaFileConfig.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | class DugaFileConfig { 4 | 5 | String deployGreeting = "Monking!" 6 | String dailyRooms = "16134,14929,24299" 7 | String undeployGoodbyeText = "TTQW!!" 8 | String deployGreetingRooms = "20298" 9 | 10 | List tasks = Arrays.asList( 11 | new TaskData("10 0 0 * * *", "dailyStats;16134,14929"), 12 | new TaskData("0 0 * * * *", "github"), 13 | new TaskData("0 * * * * *", "comments"), 14 | new TaskData("4 0 0 * * *", "unanswered;8595;codereview;***RELOAD!*** There are %unanswered% unanswered questions (%percentage%% answered)"), 15 | new TaskData("0 45 23 * * *", "ratingdiff;20298;31562,23788;codereview"), 16 | new TaskData("0 0 */2 * * *", "mess;20298;The time is %time% and @Duga is alive"), 17 | new TaskData("0 */5 * * * *", "questionScan;codereview;answerInvalidation;8595") 18 | ) 19 | 20 | List followed = [] 21 | 22 | // No users at the moment. 23 | List users = [] 24 | 25 | List getTasks() { tasks } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/ChatMessageResponse.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | public class ChatMessageResponse { 4 | 5 | private final long id; 6 | private final long time; 7 | private final String fullResponse; 8 | private final Exception exception; 9 | 10 | public ChatMessageResponse(long id, long time, String fullResponse) { 11 | this.id = id; 12 | this.time = time; 13 | this.fullResponse = fullResponse; 14 | this.exception = null; 15 | } 16 | 17 | public ChatMessageResponse(String fullResponse, Exception exception) { 18 | this.id = 0; 19 | this.time = 0; 20 | this.fullResponse = fullResponse; 21 | this.exception = exception; 22 | } 23 | 24 | public long getId() { 25 | return id; 26 | } 27 | 28 | public long getTime() { 29 | return time; 30 | } 31 | 32 | public String getFullResponse() { 33 | return fullResponse; 34 | } 35 | 36 | public Exception getException() { 37 | return exception; 38 | } 39 | 40 | public boolean hasException() { 41 | return exception != null; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /duga-ktor/src/utils/stackexchange/QuestionScanTask.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils.stackexchange 2 | 3 | import net.zomis.duga.chat.DugaPoster 4 | import org.slf4j.LoggerFactory 5 | import java.time.Instant 6 | import java.time.temporal.ChronoUnit 7 | 8 | class QuestionScanTask(val poster: DugaPoster, val stackApi: StackExchangeApi, val site: String) { 9 | 10 | private val FILTER = "!DEQ-Ts0KBm6n14zYUs8UZUsw.yj0rZkhsEKF2rI4kBp*yOHv4z4" 11 | val LATEST_QUESTIONS = "questions?order=desc&sort=activity" 12 | 13 | private var logger = LoggerFactory.getLogger(this::class.java) 14 | private var lastCheck: Instant = Instant.now().minus(1, ChronoUnit.MINUTES) 15 | 16 | suspend fun run() { 17 | val previousCheck = this.lastCheck 18 | this.lastCheck = Instant.now() 19 | val questions = stackApi.apiCall(LATEST_QUESTIONS, site, FILTER) 20 | val t = questions?.get("items")?.map { it.get("creation_date")?.asLong() ?: 0 }?.maxOrNull() ?: 0 21 | logger.info("lastCheck highest post time is {}, previous lastCheck is {}", t, lastCheck.epochSecond) 22 | this.lastCheck = Instant.ofEpochSecond(maxOf(lastCheck.epochSecond, t)) 23 | AnswerInvalidationCheck.perform(poster, questions, previousCheck, stackApi) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /grails-app/views/error.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
  • An error has occurred
  • 19 |
  • Exception: ${exception}
  • 20 |
  • Message: ${message}
  • 21 |
  • Path: ${path}
  • 22 |
23 |
24 |
25 | 26 |
    27 |
  • An error has occurred
  • 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /Duga/duga-aws/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("com.github.johnrengelman.shadow").version("5.2.0") 6 | } 7 | 8 | group = "net.zomis" 9 | version = "1.0-SNAPSHOT" 10 | 11 | repositories { 12 | mavenCentral() 13 | maven(url = "http://www.zomis.net/maven/") 14 | maven(url = "http://repo.spring.io/libs-release/") // Fuel 15 | } 16 | 17 | dependencies { 18 | compile(kotlin("stdlib-jdk8")) 19 | implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.11") 20 | 21 | testImplementation("org.junit.jupiter:junit-jupiter:5.5.2") 22 | 23 | compile("com.github.kittinunf.fuel", "fuel", "2.0.1") 24 | compile("org.slf4j", "slf4j-simple", "1.7.29") 25 | compile("org.apache.commons", "commons-text", "1.8") 26 | compile("net.zomis", "duga-core", "0.4") 27 | compile("com.amazonaws", "aws-lambda-java-core", "1.2.0") 28 | compile("com.amazonaws", "aws-java-sdk-dynamodb", "1.11.675") 29 | compile("com.amazonaws", "amazon-sqs-java-messaging-lib", "1.0.4") 30 | } 31 | 32 | tasks.test { 33 | useJUnitPlatform() 34 | testLogging { 35 | events("passed", "skipped", "failed") 36 | } 37 | } 38 | 39 | tasks.withType { 40 | kotlinOptions.jvmTarget = "1.8" 41 | } -------------------------------------------------------------------------------- /grails-app/init/net/zomis/duga/Application.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | import org.springframework.context.EnvironmentAware 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.core.env.Environment 8 | import org.springframework.core.env.MapPropertySource 9 | import org.springframework.scheduling.TaskScheduler 10 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler 11 | 12 | class Application extends GrailsAutoConfiguration implements EnvironmentAware { 13 | static void main(String[] args) { 14 | GrailsApp.run(Application) 15 | } 16 | 17 | @Bean 18 | public TaskScheduler executor() { 19 | ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); 20 | scheduler.setPoolSize(3); 21 | scheduler.setThreadNamePrefix("beanscheduler-"); 22 | return scheduler; 23 | } 24 | 25 | @Override 26 | void setEnvironment(Environment environment) { 27 | def config = getClass().getClassLoader().getResource('duga.groovy') 28 | ConfigObject slurper = new ConfigSlurper().parse(config) 29 | environment.propertySources.addFirst(new MapPropertySource('duga', slurper)) 30 | } 31 | } -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/GithubHookController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.chat.BotRoom 4 | import org.grails.web.json.JSONObject 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.beans.factory.annotation.Autowired 8 | 9 | class GithubHookController { 10 | 11 | private static final Logger logger = LoggerFactory.getLogger(GithubHookController.class) 12 | 13 | static allowedMethods = [hook:'POST'] 14 | 15 | @Autowired 16 | DugaBotService bot 17 | 18 | @Autowired 19 | HookStringification stringification 20 | 21 | def hook() { 22 | String eventType = request.getHeader('X-GitHub-Event') 23 | String room = params?.roomId 24 | JSONObject json = request.JSON 25 | 26 | logger.info('Received hook: {} to room {}: {}', eventType, room, json) 27 | 28 | List strings = stringification.postGithub(eventType, json) 29 | if (room == null) { 30 | room = '16134' 31 | } 32 | String[] rooms = room.split(',') 33 | for (String postRoom : rooms) { 34 | BotRoom hookParams = bot.room(postRoom) 35 | bot.postChat(hookParams.messages(strings)) 36 | } 37 | render 'OK' 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /grails-app/views/user/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.list.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 23 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/tasks/UnansweredTask.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import net.zomis.duga.aws.DugaMessage 4 | import net.zomis.duga.utils.StackExchangeAPI 5 | import org.slf4j.LoggerFactory 6 | import java.io.IOException 7 | 8 | class UnansweredTask(private val room: String, private val site: String, private val message: String) : DugaTask { 9 | 10 | private val logger = LoggerFactory.getLogger(javaClass) 11 | 12 | private val api = StackExchangeAPI() 13 | 14 | override fun perform(): List { 15 | return try { 16 | val result = api.apiCall("info", site, "default") 17 | val unanswered = result["items"][0]["total_unanswered"].asInt() 18 | val total = result["items"][0]["total_questions"].asInt() 19 | val percentageAnswered = (total.toDouble() - unanswered.toDouble()) / total.toDouble() 20 | val percentageStr = String.format("%.4f", percentageAnswered * 100) 21 | var send = message 22 | send = send.replace("%unanswered%", unanswered.toString()) 23 | send = send.replace("%percentage%", percentageStr) 24 | listOf(DugaMessage(room, send)) 25 | } catch (e: IOException) { 26 | logger.error("Error with StackExchange API Call", e) 27 | listOf() 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/ChatBot.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | import net.zomis.duga.chat.events.DugaEvent; 4 | 5 | import java.util.List; 6 | import java.util.concurrent.Future; 7 | import java.util.function.Consumer; 8 | 9 | public interface ChatBot { 10 | 11 | @Deprecated 12 | Future> postChat(List messages); 13 | 14 | Future postAsync(ChatMessage message); 15 | 16 | /** 17 | * Try to post a message once and return result no matter what the response is 18 | * @return Response with details of success or error 19 | */ 20 | ChatMessageResponse postNowOnce(ChatMessage message); 21 | 22 | /** 23 | * Repeatedly try to post a message until either success or a serious error occurs. 24 | * @return Response with details of success or error 25 | */ 26 | ChatMessageResponse postNow(ChatMessage message); 27 | 28 | void start(); 29 | 30 | void stop(); 31 | 32 | BotRoom room(String roomId); 33 | 34 | /** 35 | * Add an event listener. Note that all events are run synchronously. 36 | * 37 | * @param eventClass Event class 38 | * @param handler Handler for the event class 39 | * @param Event class 40 | */ 41 | void registerListener(Class eventClass, Consumer handler); 42 | } 43 | -------------------------------------------------------------------------------- /grails-app/views/taskData/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.list.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 23 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/utils/DugaStats.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import net.zomis.duga.tasks.StatisticsTask 5 | 6 | class DugaStats { 7 | 8 | private fun stats(json: JsonNode, stats: Map) { 9 | val name = json.text("repository.full_name") 10 | val url = json.text("repository.html_url") 11 | StatisticsTask("", false).repository(name, url, stats) 12 | } 13 | 14 | fun addIssue(jsonNode: JsonNode, opened: Int) { 15 | if (opened > 0) { 16 | stats(jsonNode, mapOf("issues opened" to opened)) 17 | } else { 18 | stats(jsonNode, mapOf("issues closed" to -opened)) 19 | } 20 | } 21 | 22 | fun addIssueComment(jsonNode: JsonNode) { 23 | stats(jsonNode, mapOf("issue comments" to 1)) 24 | } 25 | 26 | fun addCommits(jsonNode: JsonNode, commits: List) { 27 | val details = commits.map {commit -> 28 | GitHubAPI().commitDetails(jsonNode["repository"], commit) 29 | }.filterNotNull() 30 | val commitCount = commits.size 31 | val additions = details.sumBy { it.additions } 32 | val deletions = details.sumBy { it.deletions } 33 | stats(jsonNode, mapOf("commits" to commitCount, "additions" to additions, "deletions" to deletions)) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /grails-app/views/userAuthority/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.list.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 23 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /duga-ktor/src/DugaTasks.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import net.zomis.duga.chat.DugaPoster 5 | import net.zomis.duga.utils.stackexchange.StackExchangeApi 6 | import net.zomis.duga.utils.stackexchange.CommentsScanTask 7 | import net.zomis.duga.utils.stackexchange.ProgrammersClassification 8 | import net.zomis.duga.utils.stackexchange.QuestionScanTask 9 | import org.slf4j.LoggerFactory 10 | 11 | class DugaTasks(private val poster: DugaPoster, private val stackApi: StackExchangeApi) { 12 | private val questionScanTask = QuestionScanTask(poster, stackApi, "codereview") 13 | 14 | fun commentsScanTask(scope: CoroutineScope): CommentsScanTask { 15 | val programmersClassification = try { 16 | val trainingData = this::class.java.classLoader.getResource("trainingset-programmers-comments.txt") 17 | val source = trainingData?.readText() 18 | val lines = source?.split("\n") 19 | ProgrammersClassification.machineLearning(lines ?: emptyList()) 20 | } catch (e: Exception) { 21 | LoggerFactory.getLogger(DugaTasks::class.java).warn("Unable to load machine learning classification", e) 22 | ProgrammersClassification.machineLearning(emptyList()) 23 | } 24 | return CommentsScanTask(scope, stackApi, programmersClassification, poster) 25 | } 26 | 27 | suspend fun answerInvalidation() = questionScanTask.run() 28 | 29 | } 30 | -------------------------------------------------------------------------------- /duga-ktor/src/server/webhooks/SplunkWebhook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.server.webhooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import io.ktor.application.* 5 | import io.ktor.request.* 6 | import io.ktor.response.* 7 | import io.ktor.routing.* 8 | import net.zomis.duga.chat.DugaPoster 9 | import org.slf4j.LoggerFactory 10 | 11 | object SplunkWebhook { 12 | 13 | private val logger = LoggerFactory.getLogger(SplunkWebhook::class.java) 14 | 15 | suspend fun post(poster: DugaPoster, room: String, node: JsonNode) { 16 | try { 17 | logger.info("Splunk webhook $room: $node") 18 | val message = node["result"]?.get("message")?.asText() ?: "Splunk Alert: " + node["search_name"].asText() 19 | poster.postMessage(room, message) 20 | } catch (e: Exception) { 21 | logger.warn("Unable to post Splunk webhook $room: $node", e) 22 | } 23 | // ${json.search_name} - ${json.result} 24 | } 25 | 26 | fun route(routing: Routing, poster: DugaPoster) { 27 | routing.route("/splunk") { 28 | post { 29 | // read headers, read params, read body 30 | post(poster, call.parameters["room"]!!, call.receive()) 31 | call.respond("OK") 32 | } 33 | post("/{room}") { 34 | post(poster, call.parameters["room"]!!, call.receive()) 35 | call.respond("OK") 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /grails-app/views/registration/saved.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 | Thank you for registering with @Duga!
18 | One more step remains, but don't worry, it is an easy step.
19 | Go to the Stack Exchange chat room Duga's Playground 20 | and ping @Duga with a one-time token that has been generated for you:
21 | Write the following in chat:
@Duga register '${key}'
22 |
23 | @Duga should respond to you with the result.
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/aws/DugaState.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.aws 2 | 3 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB 4 | import com.amazonaws.services.dynamodbv2.model.AttributeValue 5 | import com.amazonaws.services.dynamodbv2.model.GetItemRequest 6 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest 7 | import com.fasterxml.jackson.databind.ObjectMapper 8 | import net.zomis.duga.chat.state.BotState 9 | 10 | object DugaState { 11 | 12 | val mapper = ObjectMapper() 13 | 14 | fun readFromDB(dynamoDB: AmazonDynamoDB): BotState? { 15 | val fetchRequest = GetItemRequest("Duga-Bots", mapOf( 16 | "botname" to AttributeValue("duga"), 17 | "property" to AttributeValue("config") 18 | )) 19 | val getResult = dynamoDB.getItem(fetchRequest) 20 | val item = getResult.item ?: return null 21 | val stateString = item["state"]?.s ?: return null 22 | 23 | val stateAsString = mapper.readValue(stateString, BotState::class.java) 24 | return stateAsString 25 | } 26 | 27 | fun saveToDB(dynamoDB: AmazonDynamoDB, state: BotState) { 28 | val stateAsString = mapper.writeValueAsString(state) 29 | val request = PutItemRequest("Duga-Bots", mapOf( 30 | "botname" to AttributeValue("duga"), 31 | "property" to AttributeValue("config"), 32 | "state" to AttributeValue(stateAsString) 33 | )) 34 | dynamoDB.putItem(request) 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/utils/StackExchangeAPI.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import java.io.IOException 6 | import java.net.URL 7 | import java.util.zip.GZIPInputStream 8 | 9 | class StackExchangeAPI { 10 | 11 | private val stackAPI = System.getenv("STACKEXCHANGE_API") 12 | 13 | private fun buildURL(apiCall: String, site: String, filter: String, apiKey: String): URL { 14 | var call = apiCall 15 | if (!call.contains("?")) { 16 | call += "?dummy" 17 | } 18 | return URL("https://api.stackexchange.com/2.2/" + call 19 | + "&site=" + site 20 | + "&filter=" + filter + "&key=" + apiKey) 21 | } 22 | 23 | fun apiCall(apiCall: String, site: String, filter: String): JsonNode { 24 | val apiKey = stackAPI 25 | try { 26 | val url = buildURL(apiCall, site, filter, apiKey) 27 | val connection = url.openConnection() 28 | connection.setRequestProperty("Accept-Encoding", "identity") 29 | val stream = GZIPInputStream(connection.getInputStream()) 30 | return ObjectMapper().readTree(stream) 31 | } catch (ex: IOException) { 32 | val copy = IOException(ex.message?.replace(apiKey, "xxxxxxxxxxxxxxxx"), ex.cause) 33 | copy.stackTrace = ex.stackTrace 34 | throw copy 35 | } 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /duga-ktor/test/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import io.ktor.application.* 4 | import io.ktor.response.* 5 | import io.ktor.request.* 6 | import io.ktor.client.* 7 | import io.ktor.client.engine.apache.* 8 | import io.ktor.client.features.* 9 | import io.ktor.client.features.json.* 10 | import io.ktor.client.request.* 11 | import kotlinx.coroutines.* 12 | import io.ktor.client.engine.cio.* 13 | import io.ktor.routing.* 14 | import io.ktor.http.* 15 | import io.ktor.websocket.* 16 | import io.ktor.http.cio.websocket.* 17 | import java.time.* 18 | import io.ktor.client.features.websocket.* 19 | import io.ktor.client.features.websocket.WebSockets 20 | import io.ktor.http.cio.websocket.Frame 21 | import kotlinx.coroutines.channels.* 22 | import io.ktor.client.features.logging.* 23 | import io.ktor.client.features.UserAgent 24 | import io.ktor.client.features.BrowserUserAgent 25 | import com.fasterxml.jackson.databind.* 26 | import io.ktor.jackson.* 27 | import io.ktor.features.* 28 | import io.ktor.locations.* 29 | import org.slf4j.event.* 30 | import io.ktor.server.engine.* 31 | import kotlin.test.* 32 | import io.ktor.server.testing.* 33 | 34 | class ApplicationTest { 35 | @Test 36 | fun testRoot() { 37 | withTestApplication({ module(testing = true) }) { 38 | handleRequest(HttpMethod.Get, "/").apply { 39 | assertEquals(HttpStatusCode.OK, response.status()) 40 | assertEquals("HELLO WORLD!", response.content) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/BitbucketController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.chat.BotRoom 4 | import org.grails.web.json.JSONObject 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.beans.factory.annotation.Autowired 8 | 9 | class BitbucketController { 10 | 11 | private static final Logger logger = LoggerFactory.getLogger(BitbucketController.class); 12 | 13 | static allowedMethods = [bitbucket:'POST'] 14 | 15 | @Autowired 16 | DugaBotService bot 17 | 18 | @Autowired 19 | BitbucketStringification stringificationBitbucket 20 | 21 | def bitbucket() { 22 | String eventType = request.getHeader('X-Event-Key') 23 | String room = params?.roomId 24 | JSONObject json = request.JSON 25 | logger.info('JSON Data: ' + params) 26 | logger.info('JSON Request: ' + json) 27 | logger.info('Request: ' + request) 28 | logger.info('Room: ' + room) 29 | logger.info('Github Event: ' + eventType) 30 | 31 | List strings = stringificationBitbucket.postBitbucket(eventType, json) 32 | strings.forEach({ logger.info(it) }) 33 | if (room == null) { 34 | room = '16134' 35 | } 36 | String[] rooms = room.split(',') 37 | for (String postRoom : rooms) { 38 | BotRoom hookParams = bot.room(postRoom) 39 | bot.postChat(hookParams.messages(strings)) 40 | } 41 | render 'OK' 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /grails-app/conf/spring/resources.groovy: -------------------------------------------------------------------------------- 1 | import net.zomis.duga.BitbucketStringification 2 | import net.zomis.duga.DugaChatListener 3 | import net.zomis.duga.DugaFileConfig 4 | import net.zomis.duga.DugaGit 5 | import net.zomis.duga.DugaMachineLearning 6 | import net.zomis.duga.DugaStats 7 | import net.zomis.duga.DugaTasks 8 | import net.zomis.duga.DynamicStats 9 | import net.zomis.duga.GithubBean 10 | import net.zomis.duga.GormUserDetailsService 11 | import net.zomis.duga.HookStringification 12 | import net.zomis.duga.SecurityConfiguration 13 | import net.zomis.duga.SplunkController 14 | import net.zomis.duga.StackExchangeAPI 15 | import net.zomis.duga.tasks.ChatScrape 16 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 17 | 18 | beans = { 19 | webSecurityConfiguration(SecurityConfiguration) 20 | passwordEncoder(BCryptPasswordEncoder) 21 | userDetailsService(GormUserDetailsService) 22 | 23 | // Ordered by dependencies, thing A may be dependent on B if it is listed below B. 24 | // This ordering does not matter to Spring or Grails, only for personal convenience 25 | stackAPI(StackExchangeAPI) 26 | // dugaBot(DugaBotService) 27 | dugaConfig(DugaFileConfig) 28 | dynamicStats(DynamicStats) 29 | stats(DugaStats) 30 | stringification(HookStringification) 31 | stringificationBitbucket(BitbucketStringification) 32 | githubAPI(GithubBean) 33 | tasks(DugaTasks) 34 | chatListener(DugaChatListener) 35 | chatScrape(ChatScrape) 36 | learning(DugaMachineLearning) 37 | dugaGit(DugaGit) 38 | splunk(SplunkController) 39 | } 40 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jdk 2 | 3 | ENV CATALINA_HOME /usr/local/tomcat 4 | ENV PATH $CATALINA_HOME/bin:$PATH 5 | RUN mkdir -p "$CATALINA_HOME" 6 | WORKDIR $CATALINA_HOME 7 | 8 | # see https://www.apache.org/dist/tomcat/tomcat-8/KEYS 9 | RUN gpg --keyserver pool.sks-keyservers.net --recv-keys \ 10 | 05AB33110949707C93A279E3D3EFE6B686867BA6 \ 11 | 07E48665A34DCAFAE522E5E6266191C37C037D42 \ 12 | 47309207D818FFD8DCD3F83F1931D684307A10A5 \ 13 | 541FBE7D8F78B25E055DDEE13C370389288584E7 \ 14 | 61B832AC2F1C5A90F0F9B00A1C506407564C17A3 \ 15 | 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED \ 16 | 9BA44C2621385CB966EBA586F72C284D731FABEE \ 17 | A27677289986DB50844682F8ACB77FC2E86E29AC \ 18 | A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 \ 19 | DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 \ 20 | F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE \ 21 | F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23 22 | 23 | ENV TOMCAT_MAJOR 8 24 | ENV TOMCAT_VERSION 8.0.32 25 | ENV TOMCAT_TGZ_URL https://www.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz 26 | 27 | RUN set -x \ 28 | && curl -fSL "$TOMCAT_TGZ_URL" -o tomcat.tar.gz \ 29 | && curl -fSL "$TOMCAT_TGZ_URL.asc" -o tomcat.tar.gz.asc \ 30 | && gpg --verify tomcat.tar.gz.asc \ 31 | && tar -xvf tomcat.tar.gz --strip-components=1 \ 32 | && rm bin/*.bat \ 33 | && rm tomcat.tar.gz* 34 | 35 | EXPOSE 8080 36 | CMD ["catalina.sh", "run"] 37 | 38 | 39 | # WORKDIR /home 40 | # RUN apt-get install -y git 41 | # RUN git clone https://github.com/Zomis/Duga.git 42 | # WORKDIR Duga 43 | # ADD duga.groovy grails-app/conf/ 44 | # RUN ./gradlew war 45 | -------------------------------------------------------------------------------- /grails-app/conf/logback.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.BuildSettings 2 | import grails.util.Environment 3 | 4 | def logPath = '.' 5 | def env = System.getenv() 6 | println("LOGBACK TEST: Environment variables are $env") 7 | println("LOGBACK TEST: Current path is " + (new File(".")).getAbsolutePath()) 8 | 9 | if (env['TOMCAT_LOGS']) { 10 | logPath = env['TOMCAT_LOGS'] 11 | } 12 | logPath = "/var/lib/tomcat8/logs" 13 | 14 | // See http://logback.qos.ch/manual/groovy.html for details on configuration 15 | appender('STDOUT', ConsoleAppender) { 16 | encoder(PatternLayoutEncoder) { 17 | pattern = "[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level %logger - %msg%n" 18 | } 19 | } 20 | 21 | logger('org.springframework.boot.autoconfigure.security', INFO) 22 | 23 | appender("duga", FileAppender) { 24 | 25 | file = "$logPath/duga.log" 26 | append = true 27 | encoder(PatternLayoutEncoder) { 28 | pattern = "[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level %logger - %msg%n" 29 | } 30 | } 31 | 32 | logger("net.zomis.duga.chat.listen", DEBUG, ['duga'], false ) 33 | logger("net.zomis.duga.tasks", DEBUG, ['duga'], false ) 34 | logger("net.zomis.duga.chat", DEBUG, ['duga'], false ) 35 | root(INFO, ['duga']) 36 | 37 | if(Environment.current == Environment.PRODUCTION) { 38 | appender("FULL_STACKTRACE", FileAppender) { 39 | file = "$logPath/stacktrace.log" 40 | append = true 41 | encoder(PatternLayoutEncoder) { 42 | pattern = "[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level %logger - %msg%n" 43 | } 44 | } 45 | logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false ) 46 | } 47 | -------------------------------------------------------------------------------- /src/main/groovy/net/zomis/duga/tasks/UnansweredTask.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import net.zomis.duga.DugaBotService; 4 | 5 | import net.zomis.duga.StackExchangeAPI; 6 | import net.zomis.duga.chat.BotRoom; 7 | import org.apache.log4j.LogManager; 8 | import org.apache.log4j.Logger; 9 | 10 | public class UnansweredTask implements Runnable { 11 | private static final Logger logger = LogManager.getLogger(UnansweredTask.class); 12 | 13 | private final StackExchangeAPI api; 14 | private final BotRoom room; 15 | private final DugaBotService bot; 16 | private final String site; 17 | private final String message; 18 | 19 | public UnansweredTask(StackExchangeAPI stackAPI, String room, 20 | DugaBotService chatBot, String site, String message) { 21 | this.api = stackAPI; 22 | this.bot = chatBot; 23 | this.room = chatBot.room(room); 24 | this.site = site; 25 | this.message = message; 26 | } 27 | 28 | @Override 29 | public void run() { 30 | try { 31 | def result = api.apiCall("info", site, "default"); 32 | int unanswered = result.items[0].total_unanswered as int; 33 | int total = result.items[0].total_questions as int; 34 | String message = this.message; 35 | double percentageAnswered = (double) (total - unanswered) / total; 36 | String percentageStr = String.format("%.4f", percentageAnswered * 100); 37 | message = message.replace("%unanswered%", String.valueOf(unanswered)); 38 | message = message.replace("%percentage%", String.valueOf(percentageStr)); 39 | bot.postSingle(room, message); 40 | } catch (IOException e) { 41 | logger.error("Error with StackExchange API Call", e); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/BotRoom.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | public class BotRoom { 8 | 9 | private final ChatBot bot; 10 | private String roomId; 11 | 12 | public BotRoom(ChatBot bot, String roomId) { 13 | this.bot = bot; 14 | this.roomId = roomId; 15 | } 16 | 17 | public String getRoomId() { 18 | return roomId; 19 | } 20 | 21 | @Deprecated 22 | public static BotRoom toRoom(String roomId) { 23 | return new BotRoom(null, roomId); 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null) return false; 30 | if (getClass() != o.getClass()) return false; 31 | 32 | BotRoom that = (BotRoom) o; 33 | 34 | if (!roomId.equals(that.roomId)) return false; 35 | 36 | return true; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int result = roomId.hashCode(); 42 | return result; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "Room " + roomId; 48 | } 49 | 50 | public ChatMessage message(String input) { 51 | return new ChatMessage(bot, this, input); 52 | } 53 | 54 | public List messages(String... messages) { 55 | return Arrays.stream(messages).map(this::message).collect(Collectors.toList()); 56 | } 57 | 58 | public List messages(List messages) { 59 | return messages.stream().map(this::message).collect(Collectors.toList()); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/tasks/TaskLambda.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks 2 | 3 | import com.amazonaws.services.lambda.runtime.Context 4 | import com.amazonaws.services.lambda.runtime.RequestHandler 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import net.zomis.duga.aws.Duga 7 | 8 | class TaskLambda : RequestHandler, Map> { 9 | 10 | private val mapper = ObjectMapper() 11 | 12 | override fun handleRequest(input: Map?, context: Context?): Map { 13 | val json = mapper.readTree(mapper.writeValueAsString(input)) 14 | 15 | val type = json["type"].asText() 16 | val room = json["room"].asText() 17 | val task: DugaTask? = when (type) { 18 | "mess" -> MessageTask(room, json["message"]!!.asText()) 19 | "questionScan" -> QuestionScanTask(room, json["site"]!!.asText()) 20 | "ratingdiff" -> RatingDiffTask(room, 21 | json["site"]!!.asText(), 22 | json["users"]!!.map { it.asText() } 23 | ) 24 | "stats" -> StatisticsTask(room, json["reset"]!!.asBoolean()) 25 | "unanswered" -> UnansweredTask(room, 26 | json["site"]!!.asText(), 27 | json["message"]!!.asText() 28 | ) 29 | else -> null 30 | } 31 | 32 | if (task == null) { 33 | return mapOf("error" to "No such task: $type") 34 | } 35 | 36 | val messages = task.perform() 37 | if (!messages.isEmpty()) { 38 | Duga().sendMany(messages) 39 | } 40 | 41 | return mapOf("messages" to messages) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /grails-app/init/BootStrap.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.Environment 2 | import net.zomis.duga.Authority 3 | import net.zomis.duga.User 4 | import net.zomis.duga.UserAuthority 5 | 6 | class BootStrap { 7 | 8 | def tasks 9 | 10 | def init = { servletContext -> 11 | def users = User.list() 12 | if (users.isEmpty()) { 13 | def roleUser = new Authority(authority: 'ROLE_USER').save(failOnError: true) 14 | def roleAdmin = new Authority(authority: 'ROLE_ADMIN').save(failOnError: true) 15 | def user = new User(username: 'user', password: 'user', enabled: true, accountExpired: false, accountLocked: false, credentialsExpired: false ).save(failOnError: true) 16 | String adminPassword = 'admin' + Math.random() 17 | if (Environment.current == Environment.DEVELOPMENT) { 18 | adminPassword = 'admin' 19 | } 20 | def admin = new User(username: 'admin', password: adminPassword, enabled: true, accountExpired: false, accountLocked: false, credentialsExpired: false ).save(failOnError: true) 21 | UserAuthority.create(user, roleUser, true) 22 | UserAuthority.create(admin, roleUser, true) 23 | UserAuthority.create(admin, roleAdmin, true) 24 | } 25 | tasks.initOnce() 26 | 27 | //String externalConfig = System.getenv("DUGA_CONFIG"); 28 | URL config = getClass().getClassLoader().getResource('init-tasks.groovy') 29 | if (config) { 30 | //tasks.fromGroovyDSL(config.text) 31 | } 32 | /* if (externalConfig) { 33 | File file = new File(externalConfig) 34 | if (file.exists()) { 35 | 36 | } 37 | }*/ 38 | } 39 | def destroy = { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/net/zomis/duga/tasks/ChatScrape.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.jsoup.nodes.Document; 5 | import org.jsoup.nodes.Element; 6 | import org.jsoup.select.Elements; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class ChatScrape { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(ChatScrape.class); 17 | 18 | public String fetch(long messageId) throws IOException { 19 | String url = "https://chat.stackexchange.com/transcript/message/" + 20 | messageId + "#" + messageId; 21 | logger.info("Fetching URL " + url); 22 | Document doc = Jsoup.connect(url).get(); 23 | doc.select(".message:not(.highlight)").remove(); 24 | List texts = texts(doc); 25 | texts.forEach(logger::info); 26 | return texts.get(0); 27 | } 28 | 29 | private static List texts(Document doc) { 30 | Elements results = doc.select(".message .content"); 31 | if (results.select(".quote").size() > 0) { 32 | results = results.select(".quote"); 33 | } 34 | 35 | // Remove time stamp and comment link 36 | results.select("span.relativetime").parents().remove(); 37 | 38 | List result = new ArrayList<>(results.size()); 39 | // Remove user name and link 40 | for (Element el : results) { 41 | Element rem = el.select("a").last(); 42 | if (rem != null) { 43 | rem.remove(); 44 | } 45 | result.add(el.html().replace("\n", "")); 46 | } 47 | return result; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/StackExchangeAPI.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import groovy.json.JsonSlurper 4 | import org.springframework.core.env.Environment; 5 | 6 | import java.util.zip.GZIPInputStream; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | class StackExchangeAPI implements StackAPI { 11 | 12 | @Autowired 13 | private Environment config; 14 | 15 | def fetchComments(String site, long fromDate) { 16 | final String filter = "!Fcb8.PVyNbcSSIFtmbqhHwtwVw"; 17 | return apiCall("comments?page=1&pagesize=100&fromdate=" + fromDate + 18 | "&order=desc&sort=creation", site, filter); 19 | } 20 | 21 | private static URL buildURL(String apiCall, String site, String filter, String apiKey) 22 | throws MalformedURLException { 23 | if (!apiCall.contains("?")) { 24 | apiCall = apiCall + "?dummy"; 25 | } 26 | return new URL("https://api.stackexchange.com/2.2/" + apiCall 27 | + "&site=" + site 28 | + "&filter=" + filter + "&key=" + apiKey); 29 | } 30 | 31 | @Override 32 | def apiCall(String apiCall, String site, String filter) throws IOException { 33 | final String apiKey = config.getProperty('stackAPI'); 34 | try { 35 | URL url = buildURL(apiCall, site, filter, apiKey); 36 | URLConnection connection = url.openConnection(); 37 | connection.setRequestProperty("Accept-Encoding", "identity"); 38 | def stream = new GZIPInputStream(connection.getInputStream()) 39 | return new JsonSlurper().parse(stream) 40 | } catch (IOException ex) { 41 | IOException copy = new IOException(ex.getMessage().replaceAll(apiKey, 'xxxxxxxxxxxxxxxx'), ex.getCause()) 42 | copy.setStackTrace(ex.getStackTrace()) 43 | throw copy 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/groovy/net/zomis/duga/tasks/qscan/QuestionScanTask.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.tasks.qscan 2 | 3 | import net.zomis.duga.chat.ChatBot 4 | import net.zomis.duga.GithubBean 5 | import net.zomis.duga.HookStringification 6 | import net.zomis.duga.StackAPI 7 | import net.zomis.duga.chat.BotRoom 8 | 9 | import java.time.Instant 10 | 11 | class QuestionScanTask implements Runnable { 12 | 13 | private static final FILTER = "!DEQ-Ts0KBm6n14zYUs8UZUsw.yj0rZkhsEKF2rI4kBp*yOHv4z4" 14 | public static final String LATEST_QUESTIONS = 'questions?order=desc&sort=activity' 15 | 16 | private final StackAPI stackAPI 17 | private final GithubBean githubBean 18 | private final HookStringification hookString 19 | private final ChatBot bot 20 | private final String site 21 | private final String actions 22 | private final BotRoom params 23 | Instant lastCheck 24 | 25 | def QuestionScanTask(StackAPI stackExchangeAPI, GithubBean githubBean, 26 | HookStringification hookStringification, ChatBot dugaBot, 27 | String site, String actions, String room) { 28 | this.stackAPI = stackExchangeAPI 29 | this.githubBean = githubBean 30 | this.hookString = hookStringification 31 | this.bot = dugaBot 32 | this.site = site 33 | this.actions = actions 34 | this.params = dugaBot.room(room) 35 | this.lastCheck = Instant.now() 36 | } 37 | 38 | @Override 39 | void run() { 40 | Instant previousCheck = this.lastCheck 41 | this.lastCheck = Instant.now() 42 | def questions = stackAPI.apiCall(LATEST_QUESTIONS, site, FILTER) 43 | 44 | if (actions.contains('answerInvalidation')) { 45 | AnswerInvalidationCheck.perform(questions, previousCheck, stackAPI, bot, params) 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /duga-ktor/src/server/webhooks/AppVeyorWebhook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.server.webhooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import io.ktor.application.* 5 | import io.ktor.request.* 6 | import io.ktor.routing.* 7 | import net.zomis.duga.chat.DugaPoster 8 | import org.slf4j.LoggerFactory 9 | 10 | object AppVeyorWebhook { 11 | 12 | private val logger = LoggerFactory.getLogger(AppVeyorWebhook::class.java) 13 | 14 | fun route(routing: Routing, poster: DugaPoster) { 15 | routing.route("/appveyor") { 16 | post { 17 | post(poster, call.parameters["room"]!!, call.receive()) 18 | } 19 | } 20 | routing.route("/appveyor/{room}") { 21 | post { 22 | post(poster, call.parameters["room"]!!, call.receive()) 23 | } 24 | } 25 | } 26 | 27 | suspend fun post(poster: DugaPoster, room: String, node: JsonNode) { 28 | logger.info("Incoming $room: $node") 29 | val eventName = node.get("eventName").asText().replace('_', ' ') 30 | val event = node.get("eventData") 31 | val repository = "http://github.com/${event.get("repositoryName").asText()}" 32 | 33 | val message = "\\[[**${event.get("repositoryName").asText()}**]($repository)\\] " + 34 | "[**build #${event["buildNumber"].asText()}**](${event["buildUrl"].asText()}) for commit " + 35 | "[**${event["commitId"].asText()}**]($repository/commit/${event.get("commitId").asText()}) " + 36 | "@ [**${event["buildVersion"].asText()}**]($repository/tree/${event.get("branch").asText()}) " + 37 | eventName 38 | poster.postMessage(room, message) 39 | if (eventName == "build_failure") { 40 | poster.postMessage(room, "**BUILD FAILURE!**") 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/BotConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | public class BotConfiguration { 4 | private String rootUrl; 5 | private String chatUrl; 6 | 7 | private String botEmail; 8 | private String botPassword; 9 | 10 | private int chatThrottle = 10000; 11 | private int chatMaxBurst = 2; 12 | private int chatMinimumDelay = 500; 13 | 14 | public String getRootUrl() { 15 | return rootUrl; 16 | } 17 | 18 | public void setRootUrl(final String rootUrl) { 19 | this.rootUrl = rootUrl; 20 | } 21 | 22 | public String getChatUrl() { 23 | return chatUrl; 24 | } 25 | 26 | public void setChatUrl(final String chatUrl) { 27 | this.chatUrl = chatUrl; 28 | } 29 | 30 | public String getBotEmail() { 31 | return botEmail; 32 | } 33 | 34 | public void setBotEmail(final String botEmail) { 35 | this.botEmail = botEmail; 36 | } 37 | 38 | public String getBotPassword() { 39 | return botPassword; 40 | } 41 | 42 | public void setBotPassword(final String botPassword) { 43 | this.botPassword = botPassword; 44 | } 45 | 46 | public int getChatThrottle() { 47 | return chatThrottle; 48 | } 49 | 50 | public void setChatThrottle(final int chatThrottle) { 51 | this.chatThrottle = chatThrottle; 52 | } 53 | 54 | public int getChatMaxBurst() { 55 | return chatMaxBurst; 56 | } 57 | 58 | public void setChatMaxBurst(final int chatMaxBurst) { 59 | this.chatMaxBurst = chatMaxBurst; 60 | } 61 | 62 | public int getChatMinimumDelay() { 63 | return chatMinimumDelay; 64 | } 65 | 66 | public void setChatMinimumDelay(final int chatMinimumDelay) { 67 | this.chatMinimumDelay = chatMinimumDelay; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/utils/GitHubAPI.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.github.kittinunf.fuel.Fuel 6 | import com.github.kittinunf.fuel.core.Response 7 | import org.slf4j.LoggerFactory 8 | 9 | data class GitHubCommitDetails(val additions: Int, val deletions: Int) 10 | 11 | class GitHubAPI { 12 | 13 | private val githubAPI = System.getenv("GITHUB_API") 14 | private val authHeader = "Authorization" to "token $githubAPI" 15 | private val mapper = ObjectMapper() 16 | private val logger = LoggerFactory.getLogger(javaClass) 17 | 18 | private fun apiCall(url: String, page: Int = 1): Pair { 19 | val response = Fuel.get("https://api.github.com/$url?page=$page") 20 | .header(authHeader) 21 | .responseString() 22 | val tree = mapper.readTree(response.third.get()) 23 | return tree to response.second 24 | } 25 | 26 | fun commitDetails(repository: JsonNode, commit: JsonNode): GitHubCommitDetails? { 27 | val repoName = repository.text("full_name") 28 | val sha = if (commit.has("sha")) commit.text("sha") else commit.text("id") 29 | return try { 30 | val result = apiCall("repos/$repoName/commits/$sha") 31 | val json = result.first 32 | val additions = json["stats"]["additions"].asInt() 33 | val deletions = json["stats"]["deletions"].asInt() 34 | logger.info("Adding {} additions and {} deletions to {} for commit {}", additions, deletions, repoName, sha) 35 | GitHubCommitDetails(additions, deletions) 36 | } catch (ex: Exception) { 37 | logger.warn("Can't get commit details for ${repository.text("full_name")} $sha", ex) 38 | null 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/TestBot.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | import net.zomis.duga.chat.events.DugaEvent; 4 | 5 | import java.util.*; 6 | import java.util.concurrent.Future; 7 | import java.util.function.Consumer; 8 | import java.util.stream.Collectors; 9 | 10 | public class TestBot implements ChatBot { 11 | 12 | private Map> messages = new HashMap<>(); 13 | 14 | @Override 15 | public Future> postChat(List messages) { 16 | BotRoom params = BotRoom.toRoom(messages.get(0).getRoom()); 17 | this.messages.putIfAbsent(params, new ArrayList<>()); 18 | this.messages.get(params).addAll(messages.stream() 19 | .map(ChatMessage::getMessage) 20 | .collect(Collectors.toList())); 21 | return null; 22 | } 23 | 24 | @Override 25 | public Future postAsync(ChatMessage message) { 26 | postChat(Arrays.asList(message)); 27 | return null; 28 | } 29 | 30 | @Override 31 | public ChatMessageResponse postNowOnce(ChatMessage message) { 32 | postChat(Arrays.asList(message)); 33 | return null; 34 | } 35 | 36 | @Override 37 | public ChatMessageResponse postNow(ChatMessage message) { 38 | postChat(Arrays.asList(message)); 39 | return null; 40 | } 41 | 42 | @Override 43 | public void start() { 44 | 45 | } 46 | 47 | @Override 48 | public void stop() { 49 | 50 | } 51 | 52 | @Override 53 | public BotRoom room(String roomId) { 54 | return new BotRoom(this, roomId); 55 | } 56 | 57 | @Override 58 | public void registerListener(Class eventClass, Consumer handler) { 59 | 60 | } 61 | 62 | public Map> getMessages() { 63 | return messages; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | import java.util.concurrent.Future; 4 | import java.util.function.Consumer; 5 | 6 | import com.gistlabs.mechanize.document.json.JsonDocument; 7 | 8 | public class ChatMessage { 9 | 10 | private final String room; 11 | private final String message; 12 | private Consumer onSuccess; 13 | private final ChatBot bot; 14 | private final BotRoom params; 15 | 16 | public ChatMessage(BotRoom params, String message) { 17 | this(null, params, message); 18 | } 19 | 20 | public ChatMessage(ChatBot bot, BotRoom params, String message) { 21 | this.bot = bot; 22 | this.params = params; 23 | this.room = params.getRoomId(); 24 | this.message = message; 25 | this.onSuccess = null; 26 | } 27 | 28 | public ChatMessage(BotRoom params, String message, Consumer onSuccess) { 29 | this(null, params, message); 30 | this.onSuccess = onSuccess; 31 | } 32 | 33 | public ChatMessage createCopy(String text) { 34 | return new ChatMessage(bot, params, text); 35 | } 36 | 37 | public Future post() { 38 | return bot.postAsync(this); 39 | } 40 | 41 | public ChatMessageResponse postNow() { 42 | return bot.postNow(this); 43 | } 44 | 45 | public String getMessage() { 46 | return message; 47 | } 48 | 49 | public String getRoom() { 50 | return room; 51 | } 52 | 53 | void onSuccess(JsonDocument response) { 54 | if (onSuccess != null) { 55 | onSuccess.accept(response); 56 | } 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "ChatMessage{" + 62 | "room='" + room + '\'' + 63 | ", message='" + message + '\'' + 64 | ", params=" + params + 65 | '}'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /grails-app/views/user/show.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.show.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /grails-app/views/taskData/show.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.show.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /duga-ktor/src/utils/github/GitHubApi.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils.github 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 5 | import io.ktor.client.HttpClient 6 | import io.ktor.client.request.* 7 | import org.slf4j.LoggerFactory 8 | 9 | class GitHubApi(val client: HttpClient, gitHubKey: String?) { 10 | 11 | private val authHeader = gitHubKey?.let { "Authorization" to "token $it" } 12 | private val mapper = jacksonObjectMapper() 13 | private val logger = LoggerFactory.getLogger(javaClass) 14 | 15 | private suspend fun apiCall(url: String, page: Int = 1): JsonNode { 16 | val response = client.get("https://api.github.com/$url?page=$page") { 17 | authHeader?.also { headers.append(it.first, it.second) } 18 | } 19 | return mapper.readTree(response) 20 | } 21 | 22 | data class GitHubCommitDetails(val additions: Int, val deletions: Int) 23 | 24 | suspend fun stars(repository: String): Int { 25 | val repo = apiCall("repos/$repository") 26 | return repo["stargazers_count"].asInt() 27 | } 28 | 29 | suspend fun commitDetails(repository: JsonNode, commit: JsonNode): GitHubCommitDetails? { 30 | val repoName = repository.text("full_name") 31 | val sha = if (commit.has("sha")) commit.text("sha") else commit.text("id") 32 | return try { 33 | val json = apiCall("repos/$repoName/commits/$sha") 34 | val additions = json["stats"]["additions"].asInt() 35 | val deletions = json["stats"]["deletions"].asInt() 36 | logger.info("Adding {} additions and {} deletions to {} for commit {}", additions, deletions, repoName, sha) 37 | GitHubCommitDetails(additions, deletions) 38 | } catch (ex: Exception) { 39 | logger.warn("Can't get commit details for ${repository.text("full_name")} $sha", ex) 40 | null 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/GithubBean.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import groovy.json.JsonSlurper 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.core.env.Environment 6 | 7 | public class GithubBean { 8 | 9 | @Autowired 10 | Environment environment 11 | 12 | Object githubAPI(String path) { 13 | def apiKey = environment.getProperty('githubAPI', '') 14 | if (apiKey == '') { 15 | return false 16 | } else { 17 | def user = new User() 18 | user.apiKey = apiKey 19 | user.github(path) 20 | def json = user.github(path) 21 | return json 22 | } 23 | } 24 | 25 | public List fetchEvents(Followed follow) throws IOException { 26 | return fetchEvents(follow.getFollowType() == 1, follow.getName(), follow.getLastEventId()); 27 | } 28 | 29 | private static Object[] fetchEventsByPage(boolean user, String name, int page) throws IOException { 30 | String type = user ? "users" : "repos"; 31 | URL url = new URL("https://api.github.com/" + type + "/" + name + "/events?page=" + page); 32 | return new JsonSlurper().parse(url) 33 | } 34 | 35 | public static List fetchEvents(boolean user, String name, long lastEvent) throws IOException { 36 | int page = 1; 37 | Object[] data = fetchEventsByPage(user, name, page); 38 | if (data == null) { 39 | return null; 40 | } 41 | List list = new ArrayList(Arrays.asList(data)); 42 | 43 | if (lastEvent >= 0) { 44 | boolean foundEvent = list.stream().anyMatch({ev -> Long.parseLong(ev.id) >= lastEvent}); 45 | while (!foundEvent) { 46 | data = fetchEventsByPage(user, name, page); 47 | if (data == null) { 48 | break; 49 | } 50 | list.addAll(Arrays.asList(data)); 51 | } 52 | } 53 | list.sort(Comparator.comparingLong({event -> Long.parseLong(event.id)})); 54 | return list; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /grails-app/views/userAuthority/show.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.show.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /grails-app/views/user/create.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/DugaChatListener.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.tasks.ChatScrape 4 | import net.zomis.duga.tasks.ListenTask 5 | import org.springframework.beans.factory.InitializingBean 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.core.env.Environment 8 | import org.springframework.scheduling.TaskScheduler 9 | 10 | /** 11 | * Keeps track of what rooms to listen in and regularly tell those rooms to do their listening 12 | */ 13 | class DugaChatListener implements InitializingBean { 14 | 15 | @Autowired TaskScheduler scheduler 16 | @Autowired DugaBotService chatBot 17 | @Autowired DugaTasks tasks 18 | @Autowired Environment environment 19 | @Autowired ChatScrape chatScrape 20 | @Autowired DugaMachineLearning learning 21 | @Autowired DugaGit dugaGit 22 | 23 | private ChatCommands commands 24 | 25 | private final Map listenRooms = new HashMap<>() 26 | 27 | @Override 28 | void afterPropertiesSet() throws Exception { 29 | assert !commands 30 | commands = new ChatCommands(this) 31 | listenStart('20298') 32 | } 33 | 34 | ListenTask listenStart(String roomId) { 35 | if (listenRooms.containsKey(roomId)) { 36 | throw new IllegalStateException('Already listening in room ' + roomId) 37 | } 38 | ListenTask listenTask = new ListenTask(chatBot, roomId, commands, this) 39 | def future = scheduler.scheduleWithFixedDelay(listenTask, 3000) 40 | listenTask.future = future 41 | listenRooms.put(roomId, listenTask) 42 | return listenTask 43 | } 44 | 45 | ListenTask listenStop(long roomId) { 46 | String roomKey = String.valueOf(roomId) 47 | ListenTask task = listenRooms.get(roomKey) 48 | if (task) { 49 | task.future.cancel(true) 50 | listenRooms.remove(roomKey) 51 | } 52 | return task 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /grails-app/views/taskData/create.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/groovy/net/zomis/duga/model/GrailsUser.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.model; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.userdetails.User; 5 | 6 | import java.util.Collection; 7 | 8 | public class GrailsUser extends User { 9 | 10 | private static final long serialVersionUID = 1; 11 | 12 | private final Object id; 13 | 14 | /** 15 | * Constructor. 16 | * 17 | * @param username the username presented to the 18 | * DaoAuthenticationProvider 19 | * @param password the password that should be presented to the 20 | * DaoAuthenticationProvider 21 | * @param enabled set to true if the user is enabled 22 | * @param accountNonExpired set to true if the account has not expired 23 | * @param credentialsNonExpired set to true if the credentials have not expired 24 | * @param accountNonLocked set to true if the account is not locked 25 | * @param authorities the authorities that should be granted to the caller if they 26 | * presented the correct username and password and the user is enabled. Not null. 27 | * @param id the id of the domain class instance used to populate this 28 | */ 29 | public GrailsUser(String username, 30 | String password, 31 | boolean enabled, 32 | boolean accountNonExpired, 33 | boolean credentialsNonExpired, 34 | boolean accountNonLocked, 35 | Collection authorities, 36 | Object id) { 37 | super(username, password, enabled, accountNonExpired, credentialsNonExpired, 38 | accountNonLocked, authorities); 39 | this.id = id; 40 | } 41 | 42 | /** 43 | * Get the id. 44 | * @return the id 45 | */ 46 | public Object getId() { 47 | return id; 48 | } 49 | } -------------------------------------------------------------------------------- /duga-ktor/src/chat/StackExchangeLogin.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.request.* 5 | import io.ktor.client.request.forms.* 6 | import io.ktor.http.* 7 | import org.jsoup.Jsoup 8 | import org.slf4j.LoggerFactory 9 | 10 | data class BotConfig(val rootUrl: String, val chatUrl: String, val botEmail: String, val botPassword: String) 11 | 12 | class StackExchangeLogin(private val httpClient: HttpClient, private val config: BotConfig) { 13 | 14 | private val logger = LoggerFactory.getLogger(StackExchangeLogin::class.java) 15 | 16 | suspend fun login(): Boolean { 17 | val loginPage: String = httpClient.get("${config.rootUrl}/users/login") 18 | val fkey = Jsoup.parse(loginPage).selectFirst("input[name='fkey']").attr("value") 19 | println(fkey) 20 | 21 | val r = httpClient.post(config.rootUrl + "/users/login") { 22 | body = FormDataContent(Parameters.build { 23 | append("email", config.botEmail) 24 | append("password", config.botPassword) 25 | append("fkey", fkey) 26 | }) 27 | } 28 | println(r) 29 | println("-------") 30 | 31 | val currentUserHtml: String = httpClient.get(config.rootUrl + "/users/current") 32 | val jsoup = Jsoup.parse(currentUserHtml) 33 | println("User show new:") 34 | println(jsoup.select(".user-show-new")) 35 | println("------------") 36 | val result = jsoup.selectFirst(".js-inbox-button") 37 | return result != null 38 | } 39 | 40 | suspend fun fkeyReal(): String { 41 | val favoriteChatsHtml: String = httpClient.get(config.chatUrl + "/chats/join/favorite") 42 | val jsoup = Jsoup.parse(favoriteChatsHtml) 43 | val fkey = jsoup.select("form").last().selectFirst("#fkey").attr("value") 44 | println(jsoup.select(".topbar-menu-links")) 45 | println("----------") 46 | 47 | println(fkey) 48 | return fkey 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /duga-ktor/src/DugaMain.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import net.zomis.duga.chat.* 5 | import net.zomis.duga.server.ArgumentsCheck 6 | import net.zomis.duga.server.DugaServer 7 | import net.zomis.duga.utils.github.GitHubApi 8 | import net.zomis.duga.utils.stackexchange.StackExchangeApi 9 | import net.zomis.duga.utils.stats.DugaStatsDynamoDB 10 | import net.zomis.duga.utils.stats.DugaStatsInternalMap 11 | import net.zomis.duga.utils.stats.DugaStatsNoOp 12 | import org.slf4j.LoggerFactory 13 | import java.io.File 14 | 15 | object DugaMain { 16 | private val logger = LoggerFactory.getLogger(DugaMain::class.java) 17 | 18 | fun start(params: Array) { 19 | // Dependencies and basic setup 20 | val args = ArgumentsCheck(params.toSet()) 21 | val botConfig = jacksonObjectMapper().readValue(File("bot.secret"), BotConfig::class.java) 22 | val client = DugaClient() 23 | val bot = DugaBot(client.client, botConfig) { httpClient, config -> 24 | val se = StackExchangeLogin(httpClient, config) 25 | if (se.login()) { 26 | se.fkeyReal() 27 | } else throw RuntimeException() 28 | } 29 | val poster = if (args.contains("duga-poster")) DugaPosterImpl(bot) else LoggingPoster() 30 | val stats = when { 31 | args.contains("stats-local") -> DugaStatsInternalMap() 32 | args.contains("stats-dynamodb") -> DugaStatsDynamoDB() 33 | else -> DugaStatsNoOp() 34 | } 35 | 36 | val gitHubApi = GitHubApi(client.client, readSecret("github")) 37 | val stackExchangeApi = StackExchangeApi(client.client, readSecret("stackexchange")) 38 | 39 | DugaServer(poster, gitHubApi, stackExchangeApi, stats).start(args) 40 | logger.info("Ready") 41 | } 42 | 43 | private fun readSecret(fileName: String): String = File("$fileName.secret").readText().trim() 44 | } 45 | 46 | fun main(args: Array) { 47 | DugaMain.start(args) 48 | } 49 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_fr.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne correspond pas au pattern [{3}] 2 | default.invalid.url.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une URL valide 3 | default.invalid.creditCard.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas un numéro de carte de crédit valide 4 | default.invalid.email.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une adresse e-mail valide 5 | default.invalid.range.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}] 6 | default.invalid.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}] 7 | default.invalid.max.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}] 8 | default.invalid.min.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}] 9 | default.invalid.max.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}] 10 | default.invalid.min.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}] 11 | default.invalid.validator.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas valide 12 | default.not.inlist.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne fait pas partie de la liste [{3}] 13 | default.blank.message=La propriété [{0}] de la classe [{1}] ne peut pas être vide 14 | default.not.equal.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne peut pas être égale à [{3}] 15 | default.null.message=La propriété [{0}] de la classe [{1}] ne peut pas être nulle 16 | default.not.unique.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] doit être unique 17 | 18 | default.paginate.prev=Précédent 19 | default.paginate.next=Suivant 20 | -------------------------------------------------------------------------------- /duga-core/build.gradle: -------------------------------------------------------------------------------- 1 | group 'net.zomis' 2 | version '0.4' 3 | 4 | apply plugin: 'java' 5 | apply plugin: 'maven' 6 | 7 | sourceCompatibility = 1.8 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | configurations { 14 | deployerJars 15 | } 16 | 17 | dependencies { 18 | deployerJars 'org.apache.maven.wagon:wagon-ftp:2.2' 19 | testCompile group: 'junit', name: 'junit', version: '4.11' 20 | testCompile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.21' 21 | compile 'org.apache.commons:commons-lang3:3.4' 22 | compile 'org.apache.httpcomponents:httpclient:4.3.5' 23 | compile 'com.gistlabs:mechanize:2.0.0-RC1' 24 | compile 'commons-io:commons-io:2.4' 25 | compile 'com.fasterxml.jackson.core:jackson-core:2.7.1' 26 | compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1-1' 27 | compile 'org.slf4j:slf4j-api:1.7.21' 28 | } 29 | 30 | def getMavenSettingsCredentials() { 31 | String userHome = System.getProperty('user.home') 32 | File mavenSettings = new File(userHome, '.m2/settings.xml') 33 | if (!mavenSettings.exists()) { 34 | return [] 35 | } 36 | def xmlSlurper = new XmlSlurper() 37 | def output = xmlSlurper.parse(mavenSettings) 38 | return output."servers"."server" 39 | } 40 | 41 | def getCredentials() { 42 | def entries = getMavenSettingsCredentials() 43 | for (entry in entries) { 44 | if (entry."id".text() == 'zomisnet') { 45 | return [username: entry.username.text(), password: entry.password.text()] 46 | } 47 | } 48 | return [username: 'invalid', password: 'invalid'] 49 | } 50 | 51 | uploadArchives { 52 | def creds = getCredentials() 53 | if (!creds) { 54 | return; 55 | } 56 | repositories { 57 | mavenDeployer { 58 | configuration = configurations.deployerJars 59 | repository(url: "ftp://www.zomis.net/public_html/maven") { 60 | authentication(userName: creds["username"], password: creds["password"]) 61 | } 62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /grails-app/views/userAuthority/create.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /grails-app/assets/stylesheets/mobile.css: -------------------------------------------------------------------------------- 1 | /* Styles for mobile devices */ 2 | 3 | @media screen and (max-width: 480px) { 4 | .nav { 5 | padding: 0.5em; 6 | } 7 | 8 | .nav li { 9 | margin: 0 0.5em 0 0; 10 | padding: 0.25em; 11 | } 12 | 13 | /* Hide individual steps in pagination, just have next & previous */ 14 | .pagination .step, .pagination .currentStep { 15 | display: none; 16 | } 17 | 18 | .pagination .prevLink { 19 | float: left; 20 | } 21 | 22 | .pagination .nextLink { 23 | float: right; 24 | } 25 | 26 | /* pagination needs to wrap around floated buttons */ 27 | .pagination { 28 | overflow: hidden; 29 | } 30 | 31 | /* slightly smaller margin around content body */ 32 | fieldset, 33 | .property-list { 34 | padding: 0.3em 1em 1em; 35 | } 36 | 37 | input, textarea { 38 | width: 100%; 39 | -moz-box-sizing: border-box; 40 | -webkit-box-sizing: border-box; 41 | -ms-box-sizing: border-box; 42 | box-sizing: border-box; 43 | } 44 | 45 | select, input[type=checkbox], input[type=radio], input[type=submit], input[type=button], input[type=reset] { 46 | width: auto; 47 | } 48 | 49 | /* hide all but the first column of list tables */ 50 | .scaffold-list td:not(:first-child), 51 | .scaffold-list th:not(:first-child) { 52 | display: none; 53 | } 54 | 55 | .scaffold-list thead th { 56 | text-align: center; 57 | } 58 | 59 | /* stack form elements */ 60 | .fieldcontain { 61 | margin-top: 0.6em; 62 | } 63 | 64 | .fieldcontain label, 65 | .fieldcontain .property-label, 66 | .fieldcontain .property-value { 67 | display: block; 68 | float: none; 69 | margin: 0 0 0.25em 0; 70 | text-align: left; 71 | width: auto; 72 | } 73 | 74 | .errors ul, 75 | .message p { 76 | margin: 0.5em; 77 | } 78 | 79 | .error ul { 80 | margin-left: 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/GormUserDetailsService.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import grails.transaction.Transactional 4 | import net.zomis.duga.model.GrailsUser 5 | import org.springframework.security.core.userdetails.UserDetails 6 | import org.springframework.security.core.userdetails.UserDetailsService 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException 8 | import org.springframework.security.core.GrantedAuthority 9 | import org.springframework.security.core.authority.SimpleGrantedAuthority 10 | 11 | @Transactional 12 | class GormUserDetailsService implements UserDetailsService { 13 | 14 | @Transactional(readOnly = true, noRollbackFor = [IllegalArgumentException, UsernameNotFoundException]) 15 | UserDetails loadUserByUsername(String username, boolean loadRoles) throws UsernameNotFoundException { 16 | 17 | def user = User.findWhere(username: username) 18 | if (!user) { 19 | log.warn "User not found: $username" 20 | throw new UsernameNotFoundException('User not found') 21 | } 22 | 23 | Collection authorities = loadAuthorities(user, username, loadRoles) 24 | createUserDetails user, authorities 25 | } 26 | 27 | UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 28 | loadUserByUsername username, true 29 | } 30 | 31 | protected Collection loadAuthorities(user, String username, boolean loadRoles) { 32 | if (!loadRoles) { 33 | return [] 34 | } 35 | 36 | Collection userAuthorities = user.authorities 37 | def authorities = userAuthorities.collect { new SimpleGrantedAuthority(it.authority) } 38 | return authorities ?: [] 39 | } 40 | 41 | protected UserDetails createUserDetails(user, Collection authorities) { 42 | new GrailsUser(user.username, user.password, user.enabled, !user.accountExpired, !user.credentialsExpired, 43 | !user.accountLocked, authorities, user.id) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/AppveyorHookController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.chat.BotRoom 4 | import org.grails.web.json.JSONObject 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.http.HttpStatus 7 | import org.springframework.web.bind.annotation.* 8 | 9 | import javax.servlet.http.HttpServletRequest 10 | import java.util.logging.Level 11 | import java.util.logging.Logger 12 | 13 | /** 14 | * @author Simon Forsberg 15 | */ 16 | class AppveyorHookController { 17 | static allowedMethods = [build:'POST'] 18 | 19 | private final static Logger LOGGER = Logger.getLogger(AppveyorHookController.class.getSimpleName()); 20 | 21 | @Autowired 22 | DugaBotService chatBot; 23 | 24 | def build() { 25 | LOGGER.info('AppVeyor! JSON Data: ' + params) 26 | LOGGER.info('Request: ' + request) 27 | JSONObject buildEvent = request.JSON 28 | LOGGER.info('JSON Request: ' + buildEvent) 29 | String room = params?.roomId 30 | LOGGER.info('Room: ' + room) 31 | BotRoom params = chatBot.room(room) 32 | String eventName = buildEvent.eventName.replace('_', ' ') 33 | def event = buildEvent.eventData 34 | 35 | String repoURL = "http://github.com/$event.repositoryName" 36 | 37 | String mess = "\\[[**$event.repositoryName**]($repoURL)\\] " + 38 | "[**build #$event.buildNumber**]($event.buildUrl) for commit " + 39 | "[**$event.commitId**]($repoURL/commit/$event.commitId) " + 40 | "@ [**$event.buildVersion**]($repoURL/tree/$event.branch) " + 41 | "$eventName" 42 | chatBot.postSingle(params, mess) 43 | if (eventName == 'build_failure') { 44 | chatBot.postSingle(params, '**BUILD FAILURE!**') 45 | } 46 | render 'OK' 47 | } 48 | 49 | @ExceptionHandler(Exception.class) 50 | @ResponseStatus(HttpStatus.BAD_REQUEST) 51 | public void handleException(final Exception ex, final HttpServletRequest request) { 52 | LOGGER.log(Level.SEVERE, "exception", ex); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /duga-ktor/src/chat/listener/ChatListener.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.listener 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.cio.* 6 | import io.ktor.client.features.websocket.* 7 | import io.ktor.client.request.* 8 | import io.ktor.client.request.forms.* 9 | import io.ktor.http.* 10 | import io.ktor.http.cio.websocket.* 11 | import net.zomis.duga.chat.DugaBot 12 | 13 | class ChatListener(val duga: DugaBot) { 14 | 15 | suspend fun listenCheck(room: String) { 16 | val data = duga.httpClient.post(duga.chatUrl + "/chats/$room/events") { 17 | body = FormDataContent(Parameters.build { 18 | append("fkey", duga.fkey()) 19 | append("mode", "messages") 20 | append("msgCount", "10") 21 | }) 22 | } 23 | println(data) 24 | } 25 | 26 | private data class UrlJson(val url: String) 27 | suspend fun listenerWebsocket(room: String) { 28 | val urlJson = duga.httpClient.post(duga.chatUrl + "/ws-auth") { 29 | body = FormDataContent(Parameters.build { 30 | append("fkey", duga.fkey()) 31 | append("roomid", room) 32 | }) 33 | } 34 | println("Url Json: $urlJson") 35 | val url = jacksonObjectMapper().readValue(urlJson, UrlJson::class.java) 36 | println("Url Json: $url") 37 | 38 | val lastPart = url.url.split("/").last() 39 | val client2 = HttpClient(CIO).config { install(WebSockets) } 40 | client2.ws("${url.url}?l=121103004", {}) { 41 | // httpClient.ws(method = HttpMethod.Get, host = "chat.sockets.stackexchange.com", port = 433, path = "/events/$room/$lastPart", {}) { 42 | println("Connected") 43 | for (msg in incoming) { 44 | println("incoming: $msg") 45 | if (msg is Frame.Text) { 46 | println("Server said: " + msg.readText()) 47 | } 48 | } 49 | println("End of transmission") 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /grails-app/controllers/net/zomis/duga/RegistrationController.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import grails.transaction.Transactional 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | 7 | import static org.springframework.http.HttpStatus.* 8 | 9 | @Transactional(readOnly = true) 10 | class RegistrationController { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(RegistrationController.class) 13 | 14 | static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] 15 | 16 | def index() { 17 | render(view: 'index') 18 | } 19 | 20 | def saved() { 21 | String key = params.result 22 | logger.info('Saved, responsekey: ' + key) 23 | render(view: 'saved', model: [key: key]) 24 | } 25 | 26 | @Transactional 27 | def save() { 28 | String apiKey = params.apikey 29 | String responseKey = apiKey + Math.random() 30 | responseKey = responseKey.encodeAsMD5() 31 | User user = new User() 32 | user.apiKey = apiKey 33 | user.pingExpect = responseKey 34 | user.chatName = '' 35 | user.githubName = user.github('user').login 36 | user.username = user.githubName 37 | user.password = apiKey 38 | user.accountExpired = false 39 | user.accountLocked = true 40 | user.credentialsExpired = false 41 | user.enabled = true 42 | 43 | if (user.hasErrors()) { 44 | transactionStatus.setRollbackOnly() 45 | respond user.errors, view:'signup' 46 | return 47 | } 48 | 49 | user.save flush:true, failOnError: true 50 | UserAuthority.create(user, Authority.findByAuthority('ROLE_USER'), true) 51 | 52 | request.withFormat { 53 | form multipartForm { 54 | flash.message = message(code: 'default.created.message', args: [message(code: 'user.label', default: 'User'), user.id]) 55 | redirect action: "saved", params: [result: responseKey] 56 | } 57 | '*' { respond user, [status: CREATED] } 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Duga 2 | ======================= 3 | 4 | [A bot named Duga](http://codereview.stackexchange.com/users/51786/duga) for chat rooms on the Stack Exchange network. Responsibilities include: 5 | 6 | - When used as a Github webhook, instantly informs a chatroom about the activity 7 | - Can make requests to Github's API every now and then to check for recent events, informs a chat room if there is activity 8 | - Uses the Stack Exchange API to check comments refering users to Code Review and Programmers. Posts these comments in The 2nd Monitor and The Whiteboard, respectively. 9 | - Listens for chat commands in Duga's Playground. 10 | 11 | Old Groovy Configuration 12 | ------------- 13 | This is obsolete and `duga-ktor` should be used instead. 14 | 15 | In the directory `grails-app/conf`, create a file named `duga.groovy` 16 | 17 | // Configurations for the bot's Stack Exchange account: 18 | rootUrl = 'https://stackexchange.com' 19 | email = 'your@email.com' 20 | password = 'yourpassword' 21 | 22 | // API configuration 23 | stackAPI = 'xxxxxxxxx' 24 | githubAPI = 'xxxxxxxxx' 25 | commandPrefix = '@Duga ' // chat messages that begins with this will be considered as commands 26 | 27 | // Database configuration 28 | adminDefaultPass = 'xxxxxxxxx' // default password for username 'admin' 29 | 30 | dataSource { 31 | username = 'xxxxxxxxx' 32 | password = 'xxxxxxxxx' 33 | } 34 | 35 | Also see the bottom part of `grails-app/conf/application.groovy` for more database configuration options. 36 | 37 | Bot account setup 38 | ----------------- 39 | 40 | In order to run a StackExchange account as a chat bot, you need to follow the following steps: 41 | 42 | 1. Create a StackExchange account on https://stackexchange.com 43 | 2. Make sure you can log in to it 44 | 3. Create an account on a specific site, for example https://codereview.stackexchange.com 45 | 4. Earn 20 reputation, following the rules of the particular site 46 | 5. Log in to https://chat.stackexchange.com 47 | 6. Confirm that you can talk 48 | 49 | Build and run tests 50 | ------------------- 51 | 52 | To build the project and run all tests: 53 | 54 | ./gradlew build 55 | -------------------------------------------------------------------------------- /grails-app/views/user/signup.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /grails-app/views/user/edit.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.edit.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/RemoteBot.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | 5 | import java.io.DataOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.net.HttpURLConnection; 9 | import java.net.URL; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | public class RemoteBot { 13 | 14 | private static final String DEFAULT_DUGA_URL = 15 | "http://stats.zomis.net/GithubHookSEChatService/bot/jsonPost"; 16 | 17 | private final String apiKey; 18 | private final String url; 19 | 20 | public RemoteBot(String apiKey) { 21 | this(DEFAULT_DUGA_URL, apiKey); 22 | } 23 | 24 | public RemoteBot(String url, String apiKey) { 25 | this.url = url; 26 | this.apiKey = apiKey; 27 | } 28 | 29 | public String post(String roomId, String message) { 30 | try { 31 | URL url = new URL(this.url); 32 | HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 33 | String request = String.format("{\"roomId\": \"%s\", \"apiKey\": \"%s\"," + 34 | "\"text\": \"%s\"}", roomId, apiKey, message); 35 | byte[] postData = request.getBytes(StandardCharsets.UTF_8); 36 | conn.setDoOutput(true); 37 | conn.setInstanceFollowRedirects(false); 38 | conn.setRequestMethod("POST"); 39 | conn.setRequestProperty("Content-Type", "application/json"); 40 | conn.setRequestProperty("Content-Length", Integer.toString(postData.length)); 41 | conn.setUseCaches(false); 42 | try (DataOutputStream it = new DataOutputStream(conn.getOutputStream())) { 43 | it.write(postData, 0, postData.length); 44 | it.flush(); 45 | } 46 | InputStream is = conn.getInputStream(); 47 | String result = IOUtils.toString(is, "UTF-8"); 48 | return result; 49 | } catch (IOException e) { 50 | throw new RuntimeException(e); 51 | } 52 | // "{ \"roomId\":\"16134\", \"apiKey\":\"" + apiKey +"\"," + 53 | // '"text": "' + fname + ']()" }' 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_zh_CN.properties: -------------------------------------------------------------------------------- 1 | default.blank.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3A\u7A7A 2 | default.doesnt.match.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E\u5B9A\u4E49\u7684\u6A21\u5F0F [{3}]\u4E0D\u5339\u914D 3 | default.invalid.creditCard.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684\u4FE1\u7528\u5361\u53F7 4 | default.invalid.email.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740 5 | default.invalid.max.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927 6 | default.invalid.max.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927 7 | default.invalid.min.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F 8 | default.invalid.min.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F 9 | default.invalid.range.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] ) 10 | default.invalid.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] ) 11 | default.invalid.url.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL 12 | default.invalid.validator.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u672A\u80FD\u901A\u8FC7\u81EA\u5B9A\u4E49\u7684\u9A8C\u8BC1 13 | default.not.equal.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E[{3}]\u4E0D\u76F8\u7B49 14 | default.not.inlist.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5217\u8868\u7684\u53D6\u503C\u8303\u56F4\u5185 15 | default.not.unique.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u5FC5\u987B\u662F\u552F\u4E00\u7684 16 | default.null.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3Anull 17 | default.paginate.next=\u4E0B\u9875 18 | default.paginate.prev=\u4E0A\u9875 19 | -------------------------------------------------------------------------------- /grails-app/views/taskData/edit.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.edit.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /grails-app/views/userAuthority/edit.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.edit.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 17 |
18 |

19 | 20 |
${flash.message}
21 |
22 | 23 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/integration-test/groovy/net/zomis/duga/github/BitbucketTest.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.github 2 | 3 | import groovy.json.JsonSlurper 4 | import net.zomis.duga.BitbucketStringification 5 | import net.zomis.duga.DugaStats 6 | import net.zomis.duga.chat.TestBot 7 | import org.junit.Before 8 | import org.junit.Test 9 | 10 | class BitbucketTest { 11 | 12 | private final BitbucketStringification hook = new BitbucketStringification() 13 | 14 | @Before 15 | public void setup() { 16 | hook.stats = new DugaStats() { 17 | @Override void addCommit(def repo, def commit) {} 18 | @Override void addCommitBitbucket(Object repo, Object commit) {} 19 | @Override void addIssueComment(def Object repo) {} 20 | @Override def addIssue(def Object repo, int delta) {} 21 | } 22 | } 23 | 24 | @Test 25 | void pushCommit() { 26 | /* 27 | * repository.links.html.href 28 | * actor.username 29 | * actor.links.html.href 30 | * repository.full_name 31 | * push.changes[].old.name 32 | * push.changes[].commits[] 33 | * - hash 34 | * - links.html.href 35 | * - message 36 | * */ 37 | 38 | String type = 'repo:push' 39 | String file = 'bitbucket/push-commit1.json' 40 | List messages = ['**\\[[SimonForsberg/minesweeper-ai](https://bitbucket.org/SimonForsberg/minesweeper-ai)]** ' + 41 | '[**Simon Forsberg**](https://bitbucket.org/SimonForsberg/) pushed commit ' + 42 | '[**ac97f33**](https://bitbucket.org/SimonForsberg/minesweeper-ai/commits/ac97f33fbee6b3d2ddf27a38394db9f134c94b12) to ' + 43 | '[**master**](https://bitbucket.org/SimonForsberg/minesweeper-ai/branch/master): Fix AI_Challenger for new API'] as List 44 | def stream = getClass().classLoader.getResourceAsStream(file) 45 | assert stream : "No stream found for '$file'" 46 | def obj = new JsonSlurper().parseText(stream.text) 47 | def result = hook.postBitbucket(type, obj) 48 | //def bot = new TestBot() 49 | //def param = bot.room('hookTest') 50 | //bot.postChat(param.messages(result)) 51 | assert result == messages 52 | // assert bot.messages[param] == messages 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/listen/ListenTask.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.listen; 2 | 3 | import net.zomis.duga.chat.BotRoom; 4 | import net.zomis.duga.chat.ChatBot; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | 11 | public class ListenTask implements Runnable { 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(ListenTask.class); 14 | 15 | private static final int NUM_MESSAGES = 10; 16 | 17 | private final ChatBot bot; 18 | private final String room; 19 | private final BotRoom params; 20 | private final ChatMessageRetriever retriever; 21 | private long lastHandledId; 22 | private long lastMessageTime; 23 | private final Consumer handler; 24 | 25 | public ListenTask(ChatBot bot, ChatMessageRetriever retriever, 26 | String room, Consumer handler) { 27 | this.bot = bot; 28 | this.room = room; 29 | this.params = bot.room(room); 30 | this.retriever = retriever; 31 | this.handler = handler; 32 | } 33 | 34 | synchronized void latestMessages() { 35 | List events = retriever.fetch(room, NUM_MESSAGES); 36 | long previousId = lastHandledId; 37 | for (ChatMessageIncoming message : events) { 38 | message.bot = bot; 39 | message.params = params; 40 | if (message.getMessageId() <= lastHandledId) { 41 | continue; 42 | } 43 | lastHandledId = Math.max(lastHandledId, message.getMessageId()); 44 | lastMessageTime = Math.max(lastMessageTime, message.getTimestamp()); 45 | if (previousId <= 0) { 46 | logger.info("Previous id 0, skipping " + message.getContent()); 47 | continue; 48 | } 49 | 50 | handler.accept(message); 51 | } 52 | if (previousId <= 0) { 53 | bot.postAsync(params.message("Monking! (Duga is now listening for commands)")); 54 | } 55 | } 56 | 57 | @Override 58 | public void run() { 59 | try { 60 | latestMessages(); 61 | } catch (RuntimeException ex) { 62 | ex.printStackTrace(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | @Library('ZomisJenkins') 4 | import net.zomis.jenkins.Duga 5 | 6 | pipeline { 7 | agent any 8 | 9 | stages { 10 | stage('Build') { 11 | steps { 12 | dir('duga-ktor') { 13 | sh './gradlew shadowJar --stacktrace' 14 | } 15 | } 16 | } 17 | 18 | stage('Docker Image') { 19 | when { 20 | branch 'main' 21 | } 22 | steps { 23 | script { 24 | // Stop running containers 25 | sh 'docker ps -q --filter name="duga_bot" | xargs -r docker stop' 26 | 27 | // Deploy 28 | dir('duga-ktor') { 29 | sh 'docker build . -t duga_bot' 30 | } 31 | withCredentials([usernamePassword( 32 | credentialsId: 'AWS_CREDENTIALS', 33 | passwordVariable: 'AWS_SECRET_ACCESS_KEY', 34 | usernameVariable: 'AWS_ACCESS_KEY_ID')]) { 35 | withEnv(["ENV_AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}", "ENV_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}"]) { 36 | def result = sh(script: """docker run --network host -d --rm --name duga_bot \ 37 | -e TZ=Europe/Amsterdam \ 38 | -e AWS_SECRET_ACCESS_KEY=$ENV_AWS_SECRET_ACCESS_KEY \ 39 | -e AWS_ACCESS_KEY_ID=$ENV_AWS_ACCESS_KEY_ID \ 40 | -v /home/zomis/jenkins/duga_bot:/data/logs \ 41 | -v /etc/localtime:/etc/localtime:ro \ 42 | -w /data/logs duga_bot""", 43 | returnStdout: true) 44 | println(result) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | post { 53 | always { 54 | junit allowEmptyResults: true, testResults: '**/build/test-results/junit-platform/TEST-*.xml' 55 | } 56 | success { 57 | zpost(0) 58 | } 59 | unstable { 60 | zpost(1) 61 | } 62 | failure { 63 | zpost(2) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /duga-ktor/src/server/webhooks/GitHubWebhook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.server.webhooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import io.ktor.application.* 5 | import io.ktor.http.* 6 | import io.ktor.request.* 7 | import io.ktor.response.* 8 | import io.ktor.routing.* 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.launch 11 | import net.zomis.duga.chat.DugaPoster 12 | import net.zomis.duga.utils.github.HookString 13 | import org.slf4j.LoggerFactory 14 | 15 | class GitHubWebhook( 16 | private val poster: DugaPoster, 17 | private val hookString: HookString 18 | ) { 19 | 20 | private val logger = LoggerFactory.getLogger(GitHubWebhook::class.java) 21 | 22 | suspend fun post( 23 | call: ApplicationCall, coroutineScope: CoroutineScope, 24 | room: String?, gitHubEvent: String, jsonNode: JsonNode 25 | ) { 26 | if (room == null) { 27 | call.respond(HttpStatusCode.BadRequest, "Missing room") 28 | return 29 | } 30 | val result = hookString.postGithub(gitHubEvent, jsonNode) 31 | coroutineScope.launch { 32 | result.forEach { 33 | poster.postMessage(room, it) 34 | } 35 | } 36 | if (result.isNotEmpty()) { 37 | call.respond("OK") 38 | } else { 39 | call.respond(HttpStatusCode.NoContent) 40 | } 41 | } 42 | 43 | fun route(routing: Routing, coroutineScope: CoroutineScope) { 44 | routing.post("/github") { 45 | val room = this.call.parameters["room"] 46 | val node = this.call.receive() 47 | val gitHubEvent = call.request.header("X-GitHub-Event") 48 | logger.warn("Incoming GitHub without room in URL: $gitHubEvent $room $node") 49 | } 50 | routing.post("/github/{room}") { 51 | val room = this.call.parameters["room"] 52 | val node = this.call.receive() 53 | val gitHubEvent = call.request.header("X-GitHub-Event") 54 | logger.info("Incoming $gitHubEvent $room $node") 55 | if (gitHubEvent == null) { 56 | call.respond(HttpStatusCode.BadRequest, "Missing 'X-GitHub-Event' header") 57 | return@post 58 | } 59 | post(call, coroutineScope, room, gitHubEvent, node) 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /duga-core/src/main/java/net/zomis/duga/chat/listen/StackExchangeFetch.java: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.chat.listen; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.gistlabs.mechanize.Resource; 5 | import com.gistlabs.mechanize.document.json.JsonDocument; 6 | import com.gistlabs.mechanize.document.json.node.JsonNode; 7 | import com.gistlabs.mechanize.impl.MechanizeAgent; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.io.IOException; 12 | import java.io.UnsupportedEncodingException; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.function.Supplier; 17 | 18 | public class StackExchangeFetch implements ChatMessageRetriever { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(StackExchangeFetch.class); 21 | 22 | private final MechanizeAgent agent; 23 | private final Supplier fkey; 24 | 25 | public StackExchangeFetch(Supplier fkey) { 26 | this.agent = new MechanizeAgent(); 27 | this.fkey = fkey; 28 | } 29 | 30 | @Override 31 | public List fetch(String roomId, int count) { 32 | Map parameters = new HashMap<>(); 33 | if (fkey == null) { 34 | return null; 35 | } 36 | parameters.put("fkey", fkey.get()); 37 | parameters.put("mode", "messages"); 38 | parameters.put("msgCount", String.valueOf(count)); 39 | Resource response; 40 | try { 41 | response = agent.post("https://chat.stackexchange.com/chats/" + 42 | roomId + "/events", parameters); 43 | } catch (UnsupportedEncodingException e) { 44 | throw new IllegalStateException(e); 45 | } 46 | 47 | if (!(response instanceof JsonDocument)) { 48 | logger.warn("Unexpected response fetching " + roomId + ": " + response); 49 | return null; 50 | } 51 | 52 | // logger.debug("Checking for events in room " + roomId); 53 | JsonDocument jsonDocument = (JsonDocument) response; 54 | JsonNode node = jsonDocument.getRoot(); 55 | 56 | ObjectMapper mapper = new ObjectMapper(); 57 | try { 58 | ListenRoot root = mapper.readValue(node.toString(), ListenRoot.class); 59 | return root.getEvents(); 60 | } catch (IOException e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /grails-app/assets/stylesheets/errors.css: -------------------------------------------------------------------------------- 1 | h1, h2 { 2 | margin: 10px 25px 5px; 3 | } 4 | 5 | h2 { 6 | font-size: 1.1em; 7 | } 8 | 9 | .filename { 10 | font-style: italic; 11 | } 12 | 13 | .exceptionMessage { 14 | margin: 10px; 15 | border: 1px solid #000; 16 | padding: 5px; 17 | background-color: #E9E9E9; 18 | } 19 | 20 | .stack, 21 | .snippet { 22 | margin: 0 25px 10px; 23 | } 24 | 25 | .stack, 26 | .snippet { 27 | border: 1px solid #ccc; 28 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 29 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 30 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 31 | } 32 | 33 | /* error details */ 34 | .error-details { 35 | border-top: 1px solid #FFAAAA; 36 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 37 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 38 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 39 | border-bottom: 1px solid #FFAAAA; 40 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 41 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 42 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 43 | background-color:#FFF3F3; 44 | line-height: 1.5; 45 | overflow: hidden; 46 | padding: 5px; 47 | padding-left:25px; 48 | } 49 | 50 | .error-details dt { 51 | clear: left; 52 | float: left; 53 | font-weight: bold; 54 | margin-right: 5px; 55 | } 56 | 57 | .error-details dt:after { 58 | content: ":"; 59 | } 60 | 61 | .error-details dd { 62 | display: block; 63 | } 64 | 65 | /* stack trace */ 66 | .stack { 67 | padding: 5px; 68 | overflow: auto; 69 | height: 150px; 70 | } 71 | 72 | /* code snippet */ 73 | .snippet { 74 | background-color: #fff; 75 | font-family: monospace; 76 | } 77 | 78 | .snippet .line { 79 | display: block; 80 | } 81 | 82 | .snippet .lineNumber { 83 | background-color: #ddd; 84 | color: #999; 85 | display: inline-block; 86 | margin-right: 5px; 87 | padding: 0 3px; 88 | text-align: right; 89 | width: 3em; 90 | } 91 | 92 | .snippet .error { 93 | background-color: #fff3f3; 94 | font-weight: bold; 95 | } 96 | 97 | .snippet .error .lineNumber { 98 | background-color: #faa; 99 | color: #333; 100 | font-weight: bold; 101 | } 102 | 103 | .snippet .line:first-child .lineNumber { 104 | padding-top: 5px; 105 | } 106 | 107 | .snippet .line:last-child .lineNumber { 108 | padding-bottom: 5px; 109 | } -------------------------------------------------------------------------------- /Duga/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /duga-ktor/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /duga-ktor/src/server/webhooks/StatsWebhook.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.server.webhooks 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.node.ObjectNode 5 | import io.ktor.application.* 6 | import io.ktor.http.* 7 | import io.ktor.request.* 8 | import io.ktor.response.* 9 | import io.ktor.routing.* 10 | import net.zomis.duga.utils.stats.DugaStats 11 | import org.slf4j.LoggerFactory 12 | 13 | object StatsWebhook { 14 | 15 | class Config { 16 | data class ItemConfig(val authToken: String, val application: String, val url: String) 17 | val items: List = emptyList() 18 | } 19 | 20 | private val logger = LoggerFactory.getLogger(StatsWebhook::class.java) 21 | 22 | fun route(routing: Routing, stats: DugaStats, config: Config) { 23 | routing.route("/stats") { 24 | get { 25 | call.respond(HttpStatusCode.OK, stats.currentStats().associate { 26 | it.displayName to mapOf( 27 | "name" to it.displayName, 28 | "url" to it.url, 29 | "values" to it.current() 30 | ) 31 | }) 32 | } 33 | post { 34 | val currentStats = saveStats(stats, call.receive(), config) 35 | if (currentStats) { 36 | call.respond(HttpStatusCode.OK, "Stats is $currentStats") 37 | } else { 38 | call.respond(HttpStatusCode.Forbidden, "Invalid request") 39 | } 40 | } 41 | } 42 | } 43 | 44 | private fun saveStats(stats: DugaStats, node: JsonNode, config: Config): Boolean { 45 | logger.info("Incoming: {}", node) 46 | val authToken = node["authToken"].asText() 47 | val application = node["application"].asText() 48 | val statsNode = node["stats"] as ObjectNode 49 | val itemConfig = config.items.find { it.authToken == authToken && it.application == application } 50 | if (itemConfig == null) { 51 | logger.warn("No stats config found for authToken '$authToken' and application '$application'") 52 | return false 53 | } 54 | 55 | val secretKey = "$authToken/$application" 56 | val statsMap = statsNode.fields().asSequence().associate { it.key to it.value.asInt() } 57 | statsMap.forEach { 58 | stats.addKey(secretKey, application, itemConfig.url, it.key, it.value) 59 | } 60 | return true 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /duga-ktor/src/utils/stackexchange/StackExchangeApi.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.utils.stackexchange 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 5 | import io.ktor.client.* 6 | import io.ktor.client.features.* 7 | import io.ktor.client.features.get 8 | import io.ktor.client.request.* 9 | import org.slf4j.LoggerFactory 10 | import java.io.IOException 11 | import java.net.URL 12 | 13 | class StackExchangeApi(val httpClient: HttpClient, val apiKey: String?) { 14 | 15 | private val logger = LoggerFactory.getLogger(StackExchangeApi::class.java) 16 | private val mapper = jacksonObjectMapper() 17 | 18 | suspend fun fetchComments(site: String, fromDate: Long): JsonNode? { 19 | val filter = "!Fcb8.PVyNbcSSIFtmbqhHwtwVw" 20 | return apiCall("comments?page=1&pagesize=100&fromdate=" + fromDate + 21 | "&order=desc&sort=creation", site, filter) 22 | } 23 | 24 | private fun buildURL(apiPath: String, site: String, filter: String, apiKey: String): URL { 25 | val apiCall = if (!apiPath.contains("?")) "$apiPath?dummy" else apiPath 26 | return URL("https://api.stackexchange.com/2.2/" + apiCall 27 | + "&site=" + site 28 | + "&filter=" + filter + "&key=" + apiKey) 29 | } 30 | 31 | suspend fun apiCall(apiCall: String, site: String, filter: String): JsonNode? { 32 | if (apiKey == null) return null 33 | try { 34 | val url = buildURL(apiCall, site, filter, apiKey) 35 | logger.info("Stack Exchange API Call: $url") 36 | val s: String = httpClient.get(url) 37 | logger.info("Stack Exchange API Call done") 38 | return mapper.readTree(s) 39 | } catch (ex: IOException) { 40 | val copy = IOException(ex.message?.replace(apiKey, "xxxxxxxxxxxxxxxx"), ex.cause) 41 | copy.stackTrace = ex.stackTrace 42 | throw copy 43 | } 44 | } 45 | 46 | data class StackExchangeSiteStats(val unanswered: Int, val total: Int) { 47 | fun percentageAnswered(): Double = (total - unanswered) / total.toDouble() 48 | } 49 | suspend fun unanswered(site: String): StackExchangeSiteStats { 50 | val apiResult = apiCall("info", site, "default") 51 | val item = apiResult?.get("items")?.get(0) 52 | 53 | val unanswered = item?.get("total_unanswered")?.asInt() ?: 0 54 | val total = item?.get("total_questions")?.asInt() ?: 1 55 | return StackExchangeSiteStats(unanswered, total) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /grails-app/views/registration/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <g:message code="default.create.label" args="[entityName]" /> 7 | 8 | 9 | 10 | 16 |
17 |

18 | 19 |
${flash.message}
20 |
21 | 22 | 27 | 28 | 29 |

Generate a Github access token

30 | Go to Github and generate a new personal access token
31 | Make sure 'admin:repo_hook' is selected. You may uncheck everything else.
32 | Enter the generated Github access token below.
33 | Benefits of registering to @Duga are: 34 |
    35 |
  • Future-proof for when webhook link changes 36 |
  • A lot easier to add a webhook 37 |
  • Allows you to ping @Duga with commands 38 |
  • Allows more github API requests 39 |
  • Easy overview of repositories attached to @Duga 40 |
41 | Api Key: 42 |
43 | 44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/aws/Duga.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.aws 2 | 3 | import com.amazonaws.auth.DefaultAWSCredentialsProviderChain 4 | import com.amazonaws.regions.Regions 5 | import com.amazonaws.services.sqs.AmazonSQSClientBuilder 6 | import javax.jms.Session 7 | import com.amazonaws.services.sqs.model.SendMessageBatchRequestEntry 8 | import com.amazonaws.services.sqs.model.SendMessageBatchRequest 9 | 10 | class Duga { 11 | 12 | private val queueUrl = "https://sqs.eu-central-1.amazonaws.com/343175303479/Duga-Messages.fifo" 13 | 14 | fun sendMany(room: String, messages: List) { 15 | return sendMany(messages.map { DugaMessage(room, it) }) 16 | } 17 | 18 | fun sendMany(messages: List) { 19 | val chunks = messages.chunked(10) 20 | chunks.forEach {chunk -> 21 | val batch = SendMessageBatchRequest() 22 | .withQueueUrl(queueUrl) 23 | .withEntries( 24 | chunk.mapIndexed { index, dugaMessage -> 25 | SendMessageBatchRequestEntry(index.toString(), dugaMessage.message) 26 | .withMessageGroupId(dugaMessage.room) 27 | .withMessageDeduplicationId(dugaMessage.md5()) 28 | } 29 | ) 30 | 31 | val sqs = AmazonSQSClientBuilder.standard().withRegion(Regions.EU_CENTRAL_1) 32 | .withCredentials(DefaultAWSCredentialsProviderChain()).build() 33 | val result = sqs.sendMessageBatch(batch) 34 | println(result) 35 | } 36 | } 37 | 38 | fun send(message: DugaMessage) { 39 | val connection = DugaSQS().connect() 40 | val session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE) 41 | 42 | // Create a queue identity and specify the queue name to the session 43 | val queue = session.createQueue(DugaSQS().queueName) 44 | 45 | // Create a producer for the 'MyQueue' 46 | val producer = session.createProducer(queue) 47 | 48 | // Create the text message 49 | val jmsMessage = session.createTextMessage(message.message) 50 | jmsMessage.setStringProperty("JMSXGroupID", message.room) 51 | jmsMessage.setStringProperty("JMS_SQS_DeduplicationId", message.md5()) 52 | 53 | // Send the message 54 | producer.send(jmsMessage) 55 | println("JMS Message " + jmsMessage.jmsMessageID) 56 | connection.close() 57 | } 58 | 59 | } 60 | 61 | fun main(args: Array) { 62 | Duga().sendMany("16134", listOf("Hello @skiwi", "Hello @Simon", "Hello @Phrancis")) 63 | } 64 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /duga-core/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_ru.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Значение [{2}] поля [{0}] класса [{1}] не соответствует образцу [{3}] 2 | default.invalid.url.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым URL-адресом 3 | default.invalid.creditCard.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым номером кредитной карты 4 | default.invalid.email.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым e-mail адресом 5 | default.invalid.range.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в допустимый интервал от [{3}] до [{4}] 6 | default.invalid.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) не попадает в допустимый интервал от [{3}] до [{4}] 7 | default.invalid.max.message=Значение [{2}] поля [{0}] класса [{1}] больше чем максимально допустимое значение [{3}] 8 | default.invalid.min.message=Значение [{2}] поля [{0}] класса [{1}] меньше чем минимально допустимое значение [{3}] 9 | default.invalid.max.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) больше чем максимально допустимый размер [{3}] 10 | default.invalid.min.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) меньше чем минимально допустимый размер [{3}] 11 | default.invalid.validator.message=Значение [{2}] поля [{0}] класса [{1}] не допустимо 12 | default.not.inlist.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в список допустимых значений [{3}] 13 | default.blank.message=Поле [{0}] класса [{1}] не может быть пустым 14 | default.not.equal.message=Значение [{2}] поля [{0}] класса [{1}] не может быть равно [{3}] 15 | default.null.message=Поле [{0}] класса [{1}] не может иметь значение null 16 | default.not.unique.message=Значение [{2}] поля [{0}] класса [{1}] должно быть уникальным 17 | 18 | default.paginate.prev=Предыдушая страница 19 | default.paginate.next=Следующая страница 20 | 21 | # Ошибки при присвоении данных. Для точной настройки для полей классов используйте 22 | # формат "typeMismatch.$className.$propertyName" (например, typeMismatch.Book.author) 23 | typeMismatch.java.net.URL=Значение поля {0} не является допустимым URL 24 | typeMismatch.java.net.URI=Значение поля {0} не является допустимым URI 25 | typeMismatch.java.util.Date=Значение поля {0} не является допустимой датой 26 | typeMismatch.java.lang.Double=Значение поля {0} не является допустимым числом 27 | typeMismatch.java.lang.Integer=Значение поля {0} не является допустимым числом 28 | typeMismatch.java.lang.Long=Значение поля {0} не является допустимым числом 29 | typeMismatch.java.lang.Short=Значение поля {0} не является допустимым числом 30 | typeMismatch.java.math.BigDecimal=Значение поля {0} не является допустимым числом 31 | typeMismatch.java.math.BigInteger=Значение поля {0} не является допустимым числом 32 | -------------------------------------------------------------------------------- /grails-app/conf/application.groovy: -------------------------------------------------------------------------------- 1 | 2 | info { 3 | app { 4 | name = '@info.app.name@' 5 | version = '@info.app.version@' 6 | grailsVersion = '@info.app.grailsVersion@' 7 | } 8 | } 9 | 10 | spring.groovy.template['check-template-location'] = false 11 | 12 | hibernate { 13 | // naming_strategy = 'org.hibernate.cfg.DefaultNamingStrategy' 14 | cache { 15 | // use_second_level_cache = false 16 | queries = false 17 | } 18 | } 19 | 20 | grails { 21 | profile = 'web' 22 | codegen.defaultPackage = 'net.zomis.duga' 23 | mime { 24 | types = [ // the first one is the default format 25 | all : '*/*', // 'all' maps to '*' or the first available format in withFormat 26 | atom : 'application/atom+xml', 27 | css : 'text/css', 28 | csv : 'text/csv', 29 | form : 'application/x-www-form-urlencoded', 30 | html : ['text/html', 'application/xhtml+xml'], 31 | js : 'text/javascript', 32 | json : ['application/json', 'text/json'], 33 | multipartForm: 'multipart/form-data', 34 | rss : 'application/rss+xml', 35 | text : 'text/plain', 36 | hal : ['application/hal+json', 'application/hal+xml'], 37 | xml : ['text/xml', 'application/xml'] 38 | ] 39 | disable.accept.header.userAgents = ['Gecko', 'WebKit', 'Presto', 'Trident'] 40 | } 41 | 42 | urlmapping.cache.maxsize = 1000 43 | controllers.defaultScope = 'singleton' 44 | converters.encoding = 'UTF-8' 45 | views.default.codec = "html" 46 | views { 47 | // 'default' { codec = 'html' } 48 | gsp { 49 | encoding = 'UTF-8' 50 | htmlcodec = 'xml' 51 | codecs { 52 | expression = 'html' 53 | scriptlet = 'html' 54 | scriptlets = 'html' 55 | taglib = 'none' 56 | staticparts = 'none' 57 | } 58 | } 59 | } 60 | } 61 | /* 62 | dataSource { 63 | pooled = true 64 | jmxExport = true 65 | driverClassName = 'org.postgresql.Driver' 66 | // username and password should be specified in duga.groovy 67 | } 68 | 69 | environments { 70 | development { 71 | dataSource { 72 | dbCreate = 'update' 73 | url = 'jdbc:postgresql://db:5432/grails' 74 | } 75 | } 76 | production { 77 | dataSource { 78 | dbCreate = 'update' 79 | url = 'jdbc:postgresql://db:5432/grails' 80 | } 81 | } 82 | }*/ 83 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_ja.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]パターンと一致していません。 2 | default.invalid.url.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なURLではありません。 3 | default.invalid.creditCard.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なクレジットカード番号ではありません。 4 | default.invalid.email.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なメールアドレスではありません。 5 | default.invalid.range.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]範囲内を指定してください。 6 | default.invalid.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]以内を指定してください。 7 | default.invalid.max.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。 8 | default.invalid.min.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。 9 | default.invalid.max.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。 10 | default.invalid.min.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。 11 | default.invalid.validator.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、カスタムバリデーションを通過できません。 12 | default.not.inlist.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]リスト内に存在しません。 13 | default.blank.message=[{1}]クラスのプロパティ[{0}]の空白は許可されません。 14 | default.not.equal.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]と同等ではありません。 15 | default.null.message=[{1}]クラスのプロパティ[{0}]にnullは許可されません。 16 | default.not.unique.message=クラス[{1}]プロパティ[{0}]の値[{2}]は既に使用されています。 17 | 18 | default.paginate.prev=戻る 19 | default.paginate.next=次へ 20 | default.boolean.true=はい 21 | default.boolean.false=いいえ 22 | default.date.format=yyyy/MM/dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0}(id:{1})を作成しました。 26 | default.updated.message={0}(id:{1})を更新しました。 27 | default.deleted.message={0}(id:{1})を削除しました。 28 | default.not.deleted.message={0}(id:{1})は削除できませんでした。 29 | default.not.found.message={0}(id:{1})は見つかりませんでした。 30 | default.optimistic.locking.failure=この{0}は編集中に他のユーザによって先に更新されています。 31 | 32 | default.home.label=ホーム 33 | default.list.label={0}リスト 34 | default.add.label={0}を追加 35 | default.new.label={0}を新規作成 36 | default.create.label={0}を作成 37 | default.show.label={0}詳細 38 | default.edit.label={0}を編集 39 | 40 | default.button.create.label=作成 41 | default.button.edit.label=編集 42 | default.button.update.label=更新 43 | default.button.delete.label=削除 44 | default.button.delete.confirm.message=本当に削除してよろしいですか? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL={0}は有効なURLでなければなりません。 48 | typeMismatch.java.net.URI={0}は有効なURIでなければなりません。 49 | typeMismatch.java.util.Date={0}は有効な日付でなければなりません。 50 | typeMismatch.java.lang.Double={0}は有効な数値でなければなりません。 51 | typeMismatch.java.lang.Integer={0}は有効な数値でなければなりません。 52 | typeMismatch.java.lang.Long={0}は有効な数値でなければなりません。 53 | typeMismatch.java.lang.Short={0}は有効な数値でなければなりません。 54 | typeMismatch.java.math.BigDecimal={0}は有効な数値でなければなりません。 55 | typeMismatch.java.math.BigInteger={0}は有効な数値でなければなりません。 56 | -------------------------------------------------------------------------------- /grails-app/i18n/messages_pt_PT.properties: -------------------------------------------------------------------------------- 1 | # 2 | # translation by miguel.ping@gmail.com, based on pt_BR translation by Lucas Teixeira - lucastex@gmail.com 3 | # 4 | 5 | default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não corresponde ao padrão definido [{3}] 6 | default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um URL válido 7 | default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito 8 | default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido. 9 | default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está dentro dos limites de valores válidos de [{3}] a [{4}] 10 | default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] está fora dos limites de tamanho válido de [{3}] a [{4}] 11 | default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o valor máximo [{3}] 12 | default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}] 13 | default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}] 14 | default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}] 15 | default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação 16 | default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não se encontra nos valores permitidos da lista [{3}] 17 | default.blank.message=O campo [{0}] da classe [{1}] não pode ser vazio 18 | default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}] 19 | default.null.message=O campo [{0}] da classe [{1}] não pode ser vazio 20 | default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único 21 | 22 | default.paginate.prev=Anterior 23 | default.paginate.next=Próximo 24 | 25 | # Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para personalizar(eg typeMismatch.Book.author) 26 | typeMismatch.java.net.URL=O campo {0} deve ser um URL válido. 27 | typeMismatch.java.net.URI=O campo {0} deve ser um URI válido. 28 | typeMismatch.java.util.Date=O campo {0} deve ser uma data válida 29 | typeMismatch.java.lang.Double=O campo {0} deve ser um número válido. 30 | typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido. 31 | typeMismatch.java.lang.Long=O campo {0} deve ser um número valido. 32 | typeMismatch.java.lang.Short=O campo {0} deve ser um número válido. 33 | typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido. 34 | typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido. 35 | -------------------------------------------------------------------------------- /grails-app/init/net/zomis/duga/SecurityConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 8 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | 11 | import org.springframework.security.crypto.password.PasswordEncoder 12 | 13 | @Configuration 14 | @EnableWebSecurity 15 | class SecurityConfiguration extends WebSecurityConfigurerAdapter { 16 | 17 | @Override 18 | protected void configure(HttpSecurity http) throws Exception { 19 | http 20 | .authorizeRequests() 21 | .antMatchers('/admin/**').hasAnyRole('ADMIN') 22 | .antMatchers('/taskData/**').hasAnyRole('ADMIN') 23 | .antMatchers('/user/signup').permitAll() 24 | .antMatchers('/user/signupSave').permitAll() 25 | .antMatchers('/user/**').hasAnyRole('ADMIN') 26 | .antMatchers('/userAuthority/**').hasAnyRole('ADMIN') 27 | .antMatchers('/home/**').hasAnyRole('USER', 'ADMIN') 28 | .antMatchers('/').permitAll() 29 | .and() 30 | .formLogin().permitAll() 31 | .and() 32 | .logout() 33 | .logoutUrl("/logout") 34 | .logoutSuccessUrl("/").permitAll() 35 | 36 | // Added *ONLY* to display the dbConsole. 37 | // Best not to do this in production. If you need frames, it would be best to use 38 | // http.headers().frameOptions().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)); 39 | // or in Spring Security 4, changing .disable() to .sameOrigin() 40 | // http.headers().frameOptions().disable() 41 | 42 | // Again, do not do this in production unless you fully understand how to mitigate Cross-Site Request Forgery 43 | // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet 44 | http.csrf().disable() 45 | 46 | } 47 | 48 | @Autowired 49 | UserDetailsService userDetailsService 50 | 51 | @Autowired 52 | PasswordEncoder passwordEncoder 53 | 54 | @Autowired 55 | protected void globalConfigure(AuthenticationManagerBuilder auth) throws Exception { 56 | auth 57 | .userDetailsService(userDetailsService) 58 | .passwordEncoder(passwordEncoder) 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /grails-app/domain/net/zomis/duga/UserAuthority.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import org.apache.commons.lang.builder.HashCodeBuilder 4 | 5 | class UserAuthority implements Serializable { 6 | 7 | private static final long serialVersionUID = 1 8 | 9 | User user 10 | Authority authority 11 | 12 | boolean equals(other) { 13 | if (!(other instanceof UserAuthority)) { 14 | return false 15 | } 16 | 17 | other.user?.id == user?.id && 18 | other.authority?.id == authority?.id 19 | } 20 | 21 | int hashCode() { 22 | def builder = new HashCodeBuilder() 23 | if (user) builder.append(user.id) 24 | if (authority) builder.append(authority.id) 25 | builder.toHashCode() 26 | } 27 | 28 | static UserAuthority get(long userId, long authorityId) { 29 | UserAuthority.where { 30 | user == User.load(userId) && 31 | authority == Authority.load(authorityId) 32 | }.get() 33 | } 34 | 35 | static boolean exists(long userId, long authorityId) { 36 | UserAuthority.where { 37 | user == User.load(userId) && 38 | authority == Authority.load(authorityId) 39 | }.count() > 0 40 | } 41 | 42 | static UserAuthority create(User user, Authority authority, boolean flush = false) { 43 | def instance = new UserAuthority(user: user, authority: authority) 44 | instance.save(flush: flush, insert: true) 45 | instance 46 | } 47 | 48 | static boolean remove(User u, Authority r) { 49 | if (u == null || r == null) return false 50 | 51 | int rowCount = UserAuthority.where { 52 | user == User.load(u.id) && 53 | authority == Authority.load(r.id) 54 | }.deleteAll() 55 | 56 | rowCount > 0 57 | } 58 | 59 | static void removeAll(User u) { 60 | if (u == null) return 61 | 62 | UserAuthority.where { 63 | user == User.load(u.id) 64 | }.deleteAll() 65 | } 66 | 67 | static void removeAll(Authority r) { 68 | if (r == null) return 69 | 70 | UserAuthority.where { 71 | authority == Authority.load(r.id) 72 | }.deleteAll() 73 | } 74 | 75 | static constraints = { 76 | authority validator: { Authority r, UserAuthority ur -> 77 | if (ur.user == null) return 78 | boolean existing = false 79 | UserAuthority.withNewSession { 80 | existing = UserAuthority.exists(ur.user.id, r.id) 81 | } 82 | if (existing) { 83 | return 'userAuthority.exists' 84 | } 85 | } 86 | } 87 | 88 | static mapping = { 89 | id composite: ['authority', 'user'] 90 | version false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/DugaGit.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import org.apache.log4j.LogManager 4 | import org.apache.log4j.Logger 5 | import org.eclipse.jgit.api.Git 6 | import org.eclipse.jgit.lib.PersonIdent 7 | import org.eclipse.jgit.transport.PushResult 8 | import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider 9 | import org.springframework.beans.factory.InitializingBean 10 | import org.springframework.beans.factory.annotation.Autowired 11 | import org.springframework.core.env.Environment 12 | 13 | class DugaGit implements InitializingBean { 14 | 15 | private static final Logger logger = LogManager.getLogger(DugaGit.class) 16 | 17 | public static final String CONFIG_GIT_BASE_DIR = "gitBaseDir"; 18 | 19 | @Autowired 20 | Environment environment 21 | 22 | private File baseDir; 23 | 24 | @Override 25 | void afterPropertiesSet() throws Exception { 26 | String gitPath = environment.getProperty(CONFIG_GIT_BASE_DIR, "duga-git-worktrees"); 27 | baseDir = new File(gitPath); 28 | baseDir.mkdirs(); 29 | if (baseDir.exists() && baseDir.isDirectory()) { 30 | logger.info("Using git base: " + baseDir.getAbsolutePath()) 31 | } else { 32 | logger.error("Git base could not be created: " + baseDir.getAbsolutePath()) 33 | } 34 | } 35 | 36 | public Git cloneOrPull(String name, String uri) { 37 | if (!baseDir.exists() || !baseDir.isDirectory()) { 38 | throw new FileNotFoundException("baseDir not found: " + baseDir.getAbsolutePath()); 39 | } 40 | File repoDir = new File(baseDir, name); 41 | if (repoDir.exists()) { 42 | try { 43 | Git git = Git.open(repoDir); 44 | git.pull().call(); 45 | return git; 46 | } catch (IOException ex) { 47 | logger.warn("Unable to open repository " + repoDir.getAbsolutePath() + ": " + ex) 48 | } 49 | } 50 | logger.info("Cloning " + uri + " into " + repoDir.getAbsolutePath()) 51 | return Git.cloneRepository() 52 | .setURI(uri) 53 | .setDirectory(repoDir) 54 | .call() 55 | } 56 | 57 | public Iterable push(Git git) { 58 | String username = environment.getProperty("gitUsername") 59 | String password = environment.getProperty("gitPassword") 60 | return git.push() 61 | .setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password)) 62 | .call() 63 | } 64 | 65 | public PersonIdent getPersonIdent() { 66 | String name = environment.getProperty("gitCommitName", "Duga Bot") 67 | String email = environment.getProperty("gitCommitEmail", "zomisforsberg@gmail.com") 68 | return new PersonIdent(name, email); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /grails-app/services/net/zomis/duga/DugaBotService.groovy: -------------------------------------------------------------------------------- 1 | package net.zomis.duga 2 | 3 | import net.zomis.duga.chat.BotConfiguration 4 | import net.zomis.duga.chat.BotRoom 5 | import net.zomis.duga.chat.ChatBot 6 | import net.zomis.duga.chat.ChatMessage 7 | import net.zomis.duga.chat.ChatMessageResponse 8 | import net.zomis.duga.chat.StackExchangeChatBot 9 | import net.zomis.duga.chat.events.DugaEvent 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory 12 | import org.springframework.beans.factory.InitializingBean 13 | import org.springframework.beans.factory.annotation.Autowired 14 | import org.springframework.core.env.Environment 15 | 16 | import java.util.concurrent.Future 17 | import java.util.function.Consumer 18 | 19 | class DugaBotService implements ChatBot, InitializingBean { 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(DugaBotService.class) 22 | 23 | @Autowired 24 | Environment environment 25 | 26 | private StackExchangeChatBot bot 27 | 28 | @Deprecated 29 | void postSingle(BotRoom params, String message) { 30 | this.postAsync(params.message(message)) 31 | } 32 | 33 | @Override 34 | Future> postChat(List messages) { 35 | messages.each { 36 | logger.info("postChat $it to $it.room") 37 | } 38 | return bot.postChat(messages) 39 | } 40 | 41 | @Override 42 | Future postAsync(ChatMessage message) { 43 | bot.postAsync(message) 44 | } 45 | 46 | @Override 47 | ChatMessageResponse postNowOnce(ChatMessage message) { 48 | bot.postNowOnce(message) 49 | } 50 | 51 | @Override 52 | ChatMessageResponse postNow(ChatMessage message) { 53 | bot.postNow(message) 54 | } 55 | 56 | @Override 57 | void start() { 58 | bot.start() 59 | } 60 | 61 | @Override 62 | void stop() { 63 | bot.stop() 64 | } 65 | 66 | @Override 67 | BotRoom room(String s) { 68 | return bot.room(s) 69 | } 70 | 71 | @Override 72 | def void registerListener(Class eventClass, Consumer handler) { 73 | bot.registerListener(eventClass, handler) 74 | } 75 | 76 | @Override 77 | void afterPropertiesSet() throws Exception { 78 | def config = new BotConfiguration() 79 | config.rootUrl = environment.getProperty('rootUrl') 80 | config.chatUrl = 'https://chat.stackexchange.com' 81 | config.botEmail = environment.getProperty('email') 82 | config.botPassword = environment.getProperty('password') 83 | config.chatThrottle = 10000 84 | config.chatMaxBurst = 2 85 | config.chatMinimumDelay = 500 86 | bot = new StackExchangeChatBot(config) 87 | bot.start() 88 | } 89 | 90 | String fkey() { 91 | bot.getFKey() 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Duga/duga-aws/src/main/kotlin/net/zomis/duga/hooks/HookLambda.kt: -------------------------------------------------------------------------------- 1 | package net.zomis.duga.hooks 2 | 3 | import com.amazonaws.services.lambda.runtime.Context 4 | import com.amazonaws.services.lambda.runtime.RequestHandler 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import net.zomis.duga.aws.Duga 7 | import net.zomis.duga.aws.DugaMessage 8 | 9 | fun Map.path(path: String): Any? { 10 | val paths = path.split('.') 11 | var current: Any? = this 12 | for (next in paths) { 13 | if (current == null) { 14 | return null 15 | } 16 | current = (current as Map)[next] 17 | } 18 | return current 19 | } 20 | 21 | class HookLambda : RequestHandler, Map> { 22 | private val mapper = ObjectMapper() 23 | 24 | override fun handleRequest(input: Map?, context: Context?): Map { 25 | println(input!!) 26 | val type = input["path"] as String? ?: return response(400, "error" to "No type specified") 27 | val body = mapper.readTree(input["body"] as String) 28 | if (input["queryStringParameters"] == null) { 29 | return response(400, "error" to "You must specify the roomId at the end of the URL using for example `?roomId=20298`") 30 | } 31 | val queryParameters = input["queryStringParameters"] as Map<*, *> 32 | if (!queryParameters.containsKey("roomId")) { 33 | return response(400, "error" to "You must specify the roomId at the end of the URL using for example `?roomId=20298`") 34 | } 35 | val roomId = queryParameters["roomId"] as String 36 | 37 | val hook: DugaWebhook? = when (type) { 38 | "/splunk" -> SplunkHook() 39 | "/github" -> GitHubHook(input.path("headers.X-GitHub-Event") as String) 40 | // "/appveyor" -> AppVeyorHook() 41 | // "/bitbucket" -> BitBucketHook() 42 | // "/sonarqube" -> SonarQubeHook() 43 | else -> null 44 | } 45 | val messages = hook?.handle(body)?.map { DugaMessage(roomId, it) } 46 | messages?.forEach { println("Hook message: $it") } 47 | if (messages != null && messages.isNotEmpty()) { 48 | Duga().sendMany(messages) 49 | } 50 | 51 | // TODO: Message can be sent directly, without the need for passing through SQS (need to fix IAM roles though) 52 | return if (messages.isNullOrEmpty()) response(500) else response(200) 53 | } 54 | 55 | private fun response(responseCode: Int, vararg pairs: Pair): Map { 56 | val body: Map = pairs.fold(mapOf()) { r, entry -> r.plus(entry) } 57 | return mapOf( 58 | "isBase64Encoded" to false, 59 | "statusCode" to responseCode, 60 | "headers" to {}, 61 | "body" to mapper.writeValueAsString(body) 62 | ) 63 | } 64 | } -------------------------------------------------------------------------------- /duga-ktor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | val logback_version: String by project 4 | val ktor_version: String by project 5 | val kotlin_version: String by project 6 | 7 | plugins { 8 | application 9 | kotlin("jvm") version "1.6.0" 10 | id("com.github.johnrengelman.shadow").version("5.2.0") 11 | id("com.github.ben-manes.versions") version "0.39.0" 12 | } 13 | 14 | group = "net.zomis.duga" 15 | version = "0.0.1-SNAPSHOT" 16 | 17 | application { 18 | mainClassName = "net.zomis.duga.DugaMainKt" 19 | } 20 | 21 | repositories { 22 | mavenLocal() 23 | jcenter() 24 | maven { url = uri("https://www.zomis.net/maven") } 25 | maven { url = uri("https://kotlin.bintray.com/ktor") } 26 | maven { url = uri("https://kotlin.bintray.com/kotlinx") } 27 | } 28 | 29 | dependencies { 30 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") 31 | implementation("io.ktor:ktor-server-netty:$ktor_version") 32 | implementation("net.zomis:machlearn:0.1.0-SNAPSHOT") 33 | implementation("org.apache.commons:commons-text:1.9") 34 | implementation("org.apache.logging.log4j:log4j-core:2.17.2") 35 | implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.17.2") 36 | implementation("io.ktor:ktor-client-core:$ktor_version") 37 | implementation("io.ktor:ktor-client-core-jvm:$ktor_version") 38 | implementation("io.ktor:ktor-client-apache:$ktor_version") 39 | implementation("io.ktor:ktor-client-json-jvm:$ktor_version") 40 | implementation("io.ktor:ktor-client-gson:$ktor_version") 41 | implementation("io.ktor:ktor-client-cio:$ktor_version") 42 | implementation("io.ktor:ktor-server-core:$ktor_version") 43 | implementation("io.ktor:ktor-websockets:$ktor_version") 44 | implementation("io.ktor:ktor-client-encoding:$ktor_version") 45 | implementation("io.ktor:ktor-client-websockets:$ktor_version") 46 | implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") 47 | implementation("io.ktor:ktor-jackson:$ktor_version") 48 | implementation("io.ktor:ktor-locations:$ktor_version") 49 | implementation("io.ktor:ktor-metrics:$ktor_version") 50 | implementation("io.ktor:ktor-server-host-common:$ktor_version") 51 | implementation("org.jsoup:jsoup:1.13.1") 52 | implementation("com.github.shyiko.skedule:skedule:0.4.0") 53 | 54 | implementation("software.amazon.awssdk:dynamodb-enhanced:2.16.80") 55 | implementation("software.amazon.awssdk:apache-client:2.16.80") 56 | implementation("software.amazon.awssdk:sdk-core:2.16.80") 57 | 58 | testImplementation("io.ktor:ktor-server-tests:$ktor_version") 59 | } 60 | 61 | tasks.withType { 62 | kotlinOptions.jvmTarget = "11" 63 | } 64 | 65 | kotlin.sourceSets["main"].kotlin.srcDirs("src") 66 | kotlin.sourceSets["test"].kotlin.srcDirs("test") 67 | 68 | sourceSets["main"].resources.srcDirs("resources") 69 | sourceSets["test"].resources.srcDirs("testresources") 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.kotlin_module 2 | 3 | ### Duga-specific 4 | 5 | duga.groovy 6 | duga.conf 7 | 8 | 9 | docker/webapp/ 10 | duga-git-worktrees/ 11 | 12 | # Created by https://www.gitignore.io/api/java,gradle,grails 13 | 14 | ### Java ### 15 | *.class 16 | 17 | # Mobile Tools for Java (J2ME) 18 | .mtj.tmp/ 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.ear 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | 28 | 29 | ### Gradle ### 30 | .gradle 31 | build/ 32 | 33 | # Ignore Gradle GUI config 34 | gradle-app.setting 35 | 36 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 37 | !gradle-wrapper.jar 38 | 39 | # Cache of project 40 | .gradletasknamecache 41 | 42 | 43 | ### Grails ### 44 | # .gitignore for Grails 1.2 and 1.3 45 | # Although this should work for most versions of grails, it is 46 | # suggested that you use the "grails integrate-with --git" command 47 | # to generate your .gitignore file. 48 | 49 | # web application files 50 | /web-app/WEB-INF/classes 51 | 52 | # default HSQL database files for production mode 53 | /prodDb.* 54 | 55 | # general HSQL database files 56 | *Db.properties 57 | *Db.script 58 | 59 | # logs 60 | /stacktrace.log 61 | /test/reports 62 | /logs 63 | 64 | # project release file 65 | /*.war 66 | 67 | # plugin release files 68 | /*.zip 69 | /plugin.xml 70 | 71 | # older plugin install locations 72 | /plugins 73 | /web-app/plugins 74 | 75 | # "temporary" build files 76 | /target 77 | 78 | 79 | # Created by https://www.gitignore.io/api/intellij 80 | 81 | ### Intellij ### 82 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 83 | 84 | *.iml 85 | 86 | ## Directory-based project format: 87 | .idea/ 88 | # if you remove the above rule, at least ignore the following: 89 | 90 | # User-specific stuff: 91 | # .idea/workspace.xml 92 | # .idea/tasks.xml 93 | # .idea/dictionaries 94 | # .idea/shelf 95 | 96 | # Sensitive or high-churn files: 97 | # .idea/dataSources.ids 98 | # .idea/dataSources.xml 99 | # .idea/sqlDataSources.xml 100 | # .idea/dynamic.xml 101 | # .idea/uiDesigner.xml 102 | 103 | # Gradle: 104 | # .idea/gradle.xml 105 | # .idea/libraries 106 | 107 | # Mongo Explorer plugin: 108 | # .idea/mongoSettings.xml 109 | 110 | ## File-based project format: 111 | *.ipr 112 | *.iws 113 | 114 | ## Plugin-specific files: 115 | 116 | # IntelliJ 117 | /out/ 118 | 119 | # mpeltonen/sbt-idea plugin 120 | .idea_modules/ 121 | 122 | # JIRA plugin 123 | atlassian-ide-plugin.xml 124 | 125 | # Crashlytics plugin (for Android Studio and IntelliJ) 126 | com_crashlytics_export_strings.xml 127 | crashlytics.properties 128 | crashlytics-build.properties 129 | fabric.properties 130 | --------------------------------------------------------------------------------