diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt
index 2c161ed..f608a00 100644
--- a/src/main/kotlin/Client.kt
+++ b/src/main/kotlin/Client.kt
@@ -1,80 +1,76 @@
import io.github.cdimascio.dotenv.dotenv
-
+import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
-import jdk.nashorn.internal.parser.Token
import kotlinx.coroutines.*
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import structures.HttpProvider
-import structures.api.Response
import structures.api.account.OrgAccount
import structures.api.account.TokenData
-import java.util.*
+import structures.wrapped.Base
import kotlin.coroutines.CoroutineContext
+object Globals {
+ val eventEmitter = EventEmitter()
+ val serializer = Json {
+ ignoreUnknownKeys = true
+ prettyPrint = true
+ explicitNulls = false
+ }
+}
-object Client : CoroutineScope {
-
+class Client : CoroutineScope {
+ val api = HttpProvider(this)
private var parentJob = Job()
- val eventEmitter = EventEmitter()
- val json = Json { ignoreUnknownKeys = true }
lateinit var application : structures.api.application.Application
lateinit var orgAccount: OrgAccount
lateinit var tokenData : TokenData
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + parentJob
- private val server = embeddedServer(
- Netty,
- port = 8000,
- host = "localhost",
- parentCoroutineContext = coroutineContext
- ) {
- configureRouting()
- install(ContentNegotiation)
- }
init {
dotenv {
systemProperties = true
}
}
-
- inline fun on(
+ inline fun on(
eventName: String,
- crossinline fn: (T) -> Unit
+ crossinline fn: suspend (T) -> Unit
) {
launch {
- eventEmitter.events.collect {
+ Globals.eventEmitter.events.collect {
when (eventName) {
"pull_request" -> {
- fn(json.decodeFromString(it))
+ fn(it as T)
}
}
}
}
}
suspend fun loginAsync() {
- verifyWithJwt()
- verifyWithInstallationApp()
- getInstallationToken()
-
- }
- private suspend fun verifyWithJwt() {
- application = HttpProvider.loginIntoApplicationAsync().await()
- }
- private suspend fun verifyWithInstallationApp() {
- orgAccount = HttpProvider.loginIntoOrgAsync().await()
-
- }
- private suspend fun getInstallationToken() {
- tokenData = HttpProvider.getInstallationToken(orgAccount).await()
+ coroutineScope {
+ withContext(Dispatchers.Default) {
+ application = api.loginIntoApplicationAsync().await()
+ orgAccount = api.loginIntoOrgAsync().await()
+ tokenData = api.getInstallationTokenAsync(orgAccount).await()
+ }
+ }
}
/**
* This should start last in order to prevent blocking of thread and listeners
*/
fun startWebhookListener() {
- server.start(wait = true)
+ embeddedServer(
+ Netty,
+ port = 8000,
+ host = "localhost",
+ parentCoroutineContext = coroutineContext
+ ) {
+ configureRouting(this@Client)
+ install(ContentNegotiation) {
+ json(Globals.serializer)
+ }
+ }.start(wait = true).also { println("Started server") }
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/EventEmitter.kt b/src/main/kotlin/EventEmitter.kt
index 36f58a0..1c5177c 100644
--- a/src/main/kotlin/EventEmitter.kt
+++ b/src/main/kotlin/EventEmitter.kt
@@ -1,11 +1,13 @@
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
+import structures.api.Response
+import structures.wrapped.Base
-class EventEmitter {
- private val _events = MutableSharedFlow() // private mutable shared flow
+class EventEmitter {
+ private val _events = MutableSharedFlow() // private mutable shared flow
val events = _events.asSharedFlow() // publicly exposed as read-only shared flow
- suspend fun produceEvent(event: String) {
+ suspend fun produceEvent(event: T) {
_events.emit(event) // suspends until all subscribers receive it
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/HttpClientExtensions.kt b/src/main/kotlin/HttpClientExtensions.kt
index 51fe0ee..6be2af3 100644
--- a/src/main/kotlin/HttpClientExtensions.kt
+++ b/src/main/kotlin/HttpClientExtensions.kt
@@ -1,34 +1,49 @@
+import HashUtils.verifySignature
+import arrow.core.Either
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
+import io.ktor.util.pipeline.*
import kotlinx.coroutines.*
-import kotlinx.serialization.json.Json
-import java.io.InputStreamReader
-import java.io.Reader
+import kotlinx.serialization.decodeFromString
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor
-fun Application.configureRouting() {
+fun Application.configureRouting(client: Client) {
routing {
- pullRequests()
+
+ pullRequests(client)
}
}
-fun Routing.pullRequests() {
+fun Routing.pullRequests(client: Client) {
post("/pulls") {
val secret = call.request.headers["X-Hub-Signature-256"]!!
- val text = call.receiveText()
- if(!HashUtils.secureCompare(secret, HashUtils.sha256(text))) {
- call.respond(HttpStatusCode.Unauthorized, "Nice try")
+ val event = call.request.headers["X-Github-Event"]!!
+ val respText = call.receiveText()
+ when(val resp = verifySignature(secret, respText)) {
+ is Either.Left -> {
+ launch(Dispatchers.Default) {
+ when(event) {
+ "pull_request" -> {
+ Globals.eventEmitter.produceEvent(
+ structures.wrapped.PullRequestsManager(client,
+ Globals.serializer.decodeFromString(resp.value)
+ )
+ )
+ }
+ }
+ call.respond(HttpStatusCode.OK)
+ }.join()
+ }
+ is Either.Right -> resp.value
}
- launch(Dispatchers.Default) {
- Client.eventEmitter.produceEvent(text)
- }.join()
+
}
}
@@ -63,4 +78,11 @@ object HashUtils {
val hash: ByteArray = hasher.doFinal(body.toByteArray())
return "sha256=${hash.toHex()}"
}
+
+ suspend inline fun PipelineContext.verifySignature(secret: String, resp: String) : Either {
+ if(secureCompare(secret, sha256(resp))) {
+ return Either.Left(resp)
+ }
+ return Either.Right(call.respond(HttpStatusCode.Unauthorized, "Nice try"))
+ }
}
diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt
index f7e0ba2..d8dae0d 100644
--- a/src/main/kotlin/Main.kt
+++ b/src/main/kotlin/Main.kt
@@ -1,18 +1,23 @@
import kotlinx.coroutines.*
-import structures.api.PullRequests
+import structures.api.application.PullRequestAction
+import structures.options.PullRequestCreateOptions
+import structures.wrapped.PullRequestsManager
fun main() = runBlocking {
-
-
- Client.loginAsync()
- println(Client.orgAccount)
- Client.on("pull_request") { pr_event ->
- println(pr_event)
+ val sernClient = Client()
+ sernClient.loginAsync()
+
+ sernClient.on("pull_request") { pr_event ->
+ when(pr_event.action) {
+ PullRequestAction.Opened -> {
+ println("new pull request opened")
+ }
+ else -> Unit
+ }
}
- Client.startWebhookListener()
+ sernClient.startWebhookListener()
}
-
diff --git a/src/main/kotlin/structures/HttpProvider.kt b/src/main/kotlin/structures/HttpProvider.kt
index bb3b679..813072b 100644
--- a/src/main/kotlin/structures/HttpProvider.kt
+++ b/src/main/kotlin/structures/HttpProvider.kt
@@ -1,5 +1,5 @@
package structures
-
+import Client
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
@@ -9,29 +9,53 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
-import kotlinx.serialization.json.Json
+import kotlinx.serialization.encodeToString
+import structures.api.Response
import structures.api.account.OrgAccount
import structures.api.account.TokenData
import structures.api.application.Application
+import structures.options.PostOptions
import kotlin.coroutines.CoroutineContext
-object HttpProvider : CoroutineScope {
- private val httpClient = HttpClient(CIO) {
+class HttpProvider(private val client: Client) : CoroutineScope {
+ val httpClient = HttpClient(CIO) {
install(ContentNegotiation) {
- json(Json {
- prettyPrint = true
- })
+ json(Globals.serializer)
+ }
+ }
+ enum class ApiType {
+ Rest,
+ App
+ }
+ val orgName = "sern-handler"
+ val baseLink = "https://api.github.com"
+
+ fun authHeader(type: ApiType): Pair {
+ return HttpHeaders.Authorization to when(type) {
+ ApiType.App -> "Bearer ${JWTProvider.jwt}"
+ ApiType.Rest -> "Bearer ${client.tokenData.token}"
+ }
+ }
+ val contentTypeHeader = HttpHeaders.Accept to "application/vnd.github+json"
+ inline fun postAsync(path: String, body: V) : Deferred {
+ return async {
+ httpClient.post("$baseLink/$path") {
+ headers {
+ append(HttpHeaders.ContentType, "application/json")
+ append(contentTypeHeader)
+ append(authHeader(ApiType.Rest))
+ }
+ setBody(body)
+ }.body()
}
}
- private const val orgName = "sern-handler"
- private const val baseLink = "https://api.github.com"
fun loginIntoApplicationAsync() : Deferred {
return async {
httpClient.request("$baseLink/app") {
headers {
- append(HttpHeaders.Accept, "application/vnd.github+json")
- append(HttpHeaders.Authorization, "Bearer ${JWTProvider.jwt}")
+ append(contentTypeHeader)
+ append(authHeader(ApiType.App))
}
}.body()
}
@@ -41,23 +65,27 @@ object HttpProvider : CoroutineScope {
// GHAppInstallation
httpClient.request("$baseLink/orgs/$orgName/installation") {
headers {
- append(HttpHeaders.Accept, "application/vnd.github+json")
- append(HttpHeaders.Authorization, "Bearer ${JWTProvider.jwt}")
+ append(contentTypeHeader)
+ append(authHeader(ApiType.App))
}
}.body()
}
}
- fun getInstallationToken(orgAccount: OrgAccount): Deferred {
+ fun getInstallationTokenAsync(orgAccount: OrgAccount): Deferred {
return async {
httpClient.post("$baseLink/app/installations/${orgAccount.id}/access_tokens") {
headers {
- append(HttpHeaders.Accept, "application/vnd.github+json")
- append(HttpHeaders.Authorization, "Bearer ${JWTProvider.jwt}")
+ append(contentTypeHeader)
+ append(authHeader(ApiType.App))
}
}.body()
}
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + Job()
-}
\ No newline at end of file
+}
+
+fun HeadersBuilder.append(name: Pair) {
+ append(name.first, name.second)
+}
diff --git a/src/main/kotlin/structures/api/PullRequest.kt b/src/main/kotlin/structures/api/PullRequest.kt
index aab1173..d303b96 100644
--- a/src/main/kotlin/structures/api/PullRequest.kt
+++ b/src/main/kotlin/structures/api/PullRequest.kt
@@ -29,7 +29,7 @@ data class PullRequest(
val labels: List