feat: can create pull requests now :D

This commit is contained in:
Jacob Nguyen
2022-09-06 22:46:48 -05:00
parent 44222d203d
commit 5fb96a2e7f
13 changed files with 311 additions and 107 deletions

124
.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

View File

@@ -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<Base>()
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 <reified T : Response> on(
inline fun <reified T : Base> 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") }
}
}

View File

@@ -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<String>() // private mutable shared flow
class EventEmitter<T : Base> {
private val _events = MutableSharedFlow<T>() // 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
}
}

View File

@@ -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<Unit, ApplicationCall>.verifySignature(secret: String, resp: String) : Either<String, Unit> {
if(secureCompare(secret, sha256(resp))) {
return Either.Left(resp)
}
return Either.Right(call.respond(HttpStatusCode.Unauthorized, "Nice try"))
}
}

View File

@@ -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 {
val sernClient = Client()
sernClient.loginAsync()
Client.loginAsync()
println(Client.orgAccount)
Client.on<PullRequests>("pull_request") { pr_event ->
println(pr_event)
sernClient.on<PullRequestsManager>("pull_request") { pr_event ->
when(pr_event.action) {
PullRequestAction.Opened -> {
println("new pull request opened")
}
else -> Unit
}
}
Client.startWebhookListener()
sernClient.startWebhookListener()
}

View File

@@ -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<String, String> {
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 <reified T : Response, reified V: PostOptions> postAsync(path: String, body: V) : Deferred<T> {
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<Application> {
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<TokenData> {
fun getInstallationTokenAsync(orgAccount: OrgAccount): Deferred<TokenData> {
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()
}
}
fun HeadersBuilder.append(name: Pair<String, String>) {
append(name.first, name.second)
}

View File

@@ -29,7 +29,7 @@ data class PullRequest(
val labels: List<Label>,
val locked: Boolean,
val maintainer_can_modify: Boolean,
val merge_commit_sha: String,
val merge_commit_sha: String?,
val mergeable: Boolean?,
val mergeable_state: String,
val merged: Boolean,
@@ -51,7 +51,7 @@ data class PullRequest(
val updated_at: String,
val url: String,
val user: User
)
) : Response()
@kotlinx.serialization.Serializable
data class MergedBy(

View File

@@ -1,12 +1,12 @@
package structures.api
@kotlinx.serialization.Serializable
data class Repo(
val allow_auto_merge: Boolean,
val allow_auto_merge: Boolean?,
val allow_forking: Boolean,
val allow_merge_commit: Boolean,
val allow_rebase_merge: Boolean,
val allow_squash_merge: Boolean,
val allow_update_branch: Boolean,
val allow_merge_commit: Boolean?,
val allow_rebase_merge: Boolean?,
val allow_squash_merge: Boolean?,
val allow_update_branch: Boolean?,
val archive_url: String,
val archived: Boolean,
val assignees_url: String,
@@ -21,7 +21,7 @@ data class Repo(
val contributors_url: String,
val created_at: String,
val default_branch: String,
val delete_branch_on_merge: Boolean,
val delete_branch_on_merge: Boolean?,
val deployments_url: String,
val description: String,
val disabled: Boolean,
@@ -41,7 +41,7 @@ data class Repo(
val has_pages: Boolean,
val has_projects: Boolean,
val has_wiki: Boolean,
val homepage: String,
val homepage: String?,
val hooks_url: String,
val html_url: String,
val id: Int,
@@ -51,11 +51,11 @@ data class Repo(
val issues_url: String,
val keys_url: String,
val labels_url: String,
val language: String,
val language: String?,
val languages_url: String,
val license: String?,
val merge_commit_message: String,
val merge_commit_title: String,
val merge_commit_message: String?,
val merge_commit_title: String?,
val merges_url: String,
val milestones_url: String,
val mirror_url: String?,
@@ -70,8 +70,8 @@ data class Repo(
val pushed_at: String,
val releases_url: String,
val size: Int,
val squash_merge_commit_message: String,
val squash_merge_commit_title: String,
val squash_merge_commit_message: String?,
val squash_merge_commit_title: String?,
val ssh_url: String,
val stargazers_count: Int,
val stargazers_url: String,
@@ -85,7 +85,7 @@ data class Repo(
val trees_url: String,
val updated_at: String,
val url: String,
val use_squash_pr_title_as_default: Boolean,
val use_squash_pr_title_as_default: Boolean?,
val visibility: String,
val watchers: Int,
val watchers_count: Int,

View File

@@ -3,4 +3,4 @@ package structures.api
import kotlinx.serialization.Serializable
@Serializable
sealed class Response
sealed class Response

View File

@@ -0,0 +1,5 @@
package structures.wrapped
import Client
open class Base(val client: Client)

View File

@@ -1,11 +0,0 @@
package structures.wrapped
class PullRequests(
private val prs: structures.api.PullRequests
) {
val action = prs.action
val head = prs.pull_request.head
val base = prs.pull_request.base
val currentRepository = Repository(prs.repository)
}

View File

@@ -0,0 +1,26 @@
package structures.wrapped
import Client
import kotlinx.coroutines.Deferred
import structures.api.PullRequest
import structures.options.PullRequestCreateOptions
import structures.api.PullRequests
class PullRequestsManager(
client: Client,
private val prs: PullRequests
) : Base(client) {
val action = prs.action
val head = prs.pull_request.head
val base = prs.pull_request.base
val currentRepository = Repository(client, prs.repository)
fun create(
repoName: String,
options: PullRequestCreateOptions
): Deferred<PullRequest> {
return client.api.postAsync("repos/${client.api.orgName}/${repoName}/pulls", options)
}
}

View File

@@ -1,6 +1,13 @@
package structures.wrapped
import Client
import structures.api.Repository
class Repository(
repository: structures.api.Repository
) {
client: Client,
repository: Repository
) : Base(client) {
val name = repository.name
val fullName = repository.full_name
}