From 36850900e2f2290f30034e673ff189e12cc201fa Mon Sep 17 00:00:00 2001 From: Daave Date: Fri, 18 Aug 2023 20:10:27 +0100 Subject: [PATCH 1/6] feat: Empty WebhookController --- .../venix/hookla/sinks/SinkHandler.scala | 5 ++++ .../hookla/sinks/WebhookController.scala | 24 +++++++++++++++++++ .../scala/venix/hookla/sinks/package.scala | 3 +++ 3 files changed, 32 insertions(+) create mode 100644 src/main/scala/venix/hookla/sinks/SinkHandler.scala create mode 100644 src/main/scala/venix/hookla/sinks/WebhookController.scala create mode 100644 src/main/scala/venix/hookla/sinks/package.scala diff --git a/src/main/scala/venix/hookla/sinks/SinkHandler.scala b/src/main/scala/venix/hookla/sinks/SinkHandler.scala new file mode 100644 index 0000000..e166f42 --- /dev/null +++ b/src/main/scala/venix/hookla/sinks/SinkHandler.scala @@ -0,0 +1,5 @@ +package venix.hookla.sinks + +trait SinkHandler { + +} diff --git a/src/main/scala/venix/hookla/sinks/WebhookController.scala b/src/main/scala/venix/hookla/sinks/WebhookController.scala new file mode 100644 index 0000000..ba3d0f1 --- /dev/null +++ b/src/main/scala/venix/hookla/sinks/WebhookController.scala @@ -0,0 +1,24 @@ +package venix.hookla.sinks + +import venix.hookla.Result +import zio.ZLayer +import zio.http.Request + +trait IWebhookController { + def handleWebhook(request: Request): Result[Unit] +} + +/** + * This class contains the handling of the webhooks that are INCOMING from sources + * such as GitHub, GitLab, BitBucket, Sonarr, Radarr, etc.. + */ +class WebhookController extends IWebhookController { + def handleWebhook(request: Request): Result[Unit] = ??? +} + +object WebhookController { + private type In = Any + private def create() = new WebhookController() + + val live: zio.ZLayer[In, Throwable, IWebhookController] = ZLayer.fromFunction(create _) +} diff --git a/src/main/scala/venix/hookla/sinks/package.scala b/src/main/scala/venix/hookla/sinks/package.scala new file mode 100644 index 0000000..1df3407 --- /dev/null +++ b/src/main/scala/venix/hookla/sinks/package.scala @@ -0,0 +1,3 @@ +package venix.hookla + +package object sinks {} From 60826d534cf4c6fa5ede1a0d34e195bd3bc2f9dd Mon Sep 17 00:00:00 2001 From: Daave Date: Fri, 18 Aug 2023 20:39:46 +0100 Subject: [PATCH 2/6] refactor: Change WebhookController too --- src/main/scala/venix/hookla/sinks/WebhookController.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/venix/hookla/sinks/WebhookController.scala b/src/main/scala/venix/hookla/sinks/WebhookController.scala index ba3d0f1..36c41a0 100644 --- a/src/main/scala/venix/hookla/sinks/WebhookController.scala +++ b/src/main/scala/venix/hookla/sinks/WebhookController.scala @@ -4,7 +4,7 @@ import venix.hookla.Result import zio.ZLayer import zio.http.Request -trait IWebhookController { +trait WebhookController { def handleWebhook(request: Request): Result[Unit] } @@ -12,13 +12,13 @@ trait IWebhookController { * This class contains the handling of the webhooks that are INCOMING from sources * such as GitHub, GitLab, BitBucket, Sonarr, Radarr, etc.. */ -class WebhookController extends IWebhookController { +private class WebhookControllerImpl extends WebhookController { def handleWebhook(request: Request): Result[Unit] = ??? } object WebhookController { private type In = Any - private def create() = new WebhookController() + private def create() = new WebhookControllerImpl() - val live: zio.ZLayer[In, Throwable, IWebhookController] = ZLayer.fromFunction(create _) + val live: zio.ZLayer[In, Throwable, WebhookController] = ZLayer.fromFunction(create _) } From 9a5b74abed34d25bc1543a436bfdba70ad472aad Mon Sep 17 00:00:00 2001 From: Daave Date: Fri, 18 Aug 2023 22:34:29 +0100 Subject: [PATCH 3/6] refactor: Change to correct folder name --- .../venix/hookla/sinks/SinkHandler.scala | 5 ---- .../hookla/sinks/WebhookController.scala | 24 ------------------- .../scala/venix/hookla/sinks/package.scala | 3 --- .../hookla/sources/SourceEventHandler.scala | 9 +++++++ .../venix/hookla/sources/SourceHandler.scala | 22 +++++++++++++++++ .../scala/venix/hookla/sources/package.scala | 3 +++ 6 files changed, 34 insertions(+), 32 deletions(-) delete mode 100644 src/main/scala/venix/hookla/sinks/SinkHandler.scala delete mode 100644 src/main/scala/venix/hookla/sinks/WebhookController.scala delete mode 100644 src/main/scala/venix/hookla/sinks/package.scala create mode 100644 src/main/scala/venix/hookla/sources/SourceEventHandler.scala create mode 100644 src/main/scala/venix/hookla/sources/SourceHandler.scala create mode 100644 src/main/scala/venix/hookla/sources/package.scala diff --git a/src/main/scala/venix/hookla/sinks/SinkHandler.scala b/src/main/scala/venix/hookla/sinks/SinkHandler.scala deleted file mode 100644 index e166f42..0000000 --- a/src/main/scala/venix/hookla/sinks/SinkHandler.scala +++ /dev/null @@ -1,5 +0,0 @@ -package venix.hookla.sinks - -trait SinkHandler { - -} diff --git a/src/main/scala/venix/hookla/sinks/WebhookController.scala b/src/main/scala/venix/hookla/sinks/WebhookController.scala deleted file mode 100644 index 36c41a0..0000000 --- a/src/main/scala/venix/hookla/sinks/WebhookController.scala +++ /dev/null @@ -1,24 +0,0 @@ -package venix.hookla.sinks - -import venix.hookla.Result -import zio.ZLayer -import zio.http.Request - -trait WebhookController { - def handleWebhook(request: Request): Result[Unit] -} - -/** - * This class contains the handling of the webhooks that are INCOMING from sources - * such as GitHub, GitLab, BitBucket, Sonarr, Radarr, etc.. - */ -private class WebhookControllerImpl extends WebhookController { - def handleWebhook(request: Request): Result[Unit] = ??? -} - -object WebhookController { - private type In = Any - private def create() = new WebhookControllerImpl() - - val live: zio.ZLayer[In, Throwable, WebhookController] = ZLayer.fromFunction(create _) -} diff --git a/src/main/scala/venix/hookla/sinks/package.scala b/src/main/scala/venix/hookla/sinks/package.scala deleted file mode 100644 index 1df3407..0000000 --- a/src/main/scala/venix/hookla/sinks/package.scala +++ /dev/null @@ -1,3 +0,0 @@ -package venix.hookla - -package object sinks {} diff --git a/src/main/scala/venix/hookla/sources/SourceEventHandler.scala b/src/main/scala/venix/hookla/sources/SourceEventHandler.scala new file mode 100644 index 0000000..8b4465a --- /dev/null +++ b/src/main/scala/venix/hookla/sources/SourceEventHandler.scala @@ -0,0 +1,9 @@ +package venix.hookla.sources + +import venix.hookla.Task +import venix.hookla.models.Hook +import zio.http.Request + +trait SourceEventHandler { + def handle(request: Request, hook: Hook): Task[Unit] +} diff --git a/src/main/scala/venix/hookla/sources/SourceHandler.scala b/src/main/scala/venix/hookla/sources/SourceHandler.scala new file mode 100644 index 0000000..7e1c8e9 --- /dev/null +++ b/src/main/scala/venix/hookla/sources/SourceHandler.scala @@ -0,0 +1,22 @@ +package venix.hookla.sources + +import zio.{URIO, ZIO} +import zio.http.Request + +trait SourceHandler { + /* + * This method is called when a webhook is received. + * It should determine the event type and then call the appropriate method. + * The event type is determined by the source, and the source is determined by the request. + * The request is passed in so that the handler can determine the event type. + * i.e. push, issue, deployment, etc... + */ + def determineEvent(req: Request): SourceEventHandler +} + +object SourceHandler { + def getHandlerById(id: String): URIO[Any, SourceHandler] = + id match { + case "github" => ??? + } +} diff --git a/src/main/scala/venix/hookla/sources/package.scala b/src/main/scala/venix/hookla/sources/package.scala new file mode 100644 index 0000000..5d3f2ff --- /dev/null +++ b/src/main/scala/venix/hookla/sources/package.scala @@ -0,0 +1,3 @@ +package venix.hookla + +package object sources {} From 926fb9789e5f6ab95e57fd76a397929a7afdc516 Mon Sep 17 00:00:00 2001 From: Daave Date: Fri, 18 Aug 2023 22:34:57 +0100 Subject: [PATCH 4/6] feat: Start webhook handler endpoint app --- src/main/scala/venix/hookla/App.scala | 16 +++-- src/main/scala/venix/hookla/package.scala | 3 +- .../hookla/services/db/HookService.scala | 10 +++ .../hookla/sources/WebhookController.scala | 63 +++++++++++++++++++ 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/main/scala/venix/hookla/sources/WebhookController.scala diff --git a/src/main/scala/venix/hookla/App.scala b/src/main/scala/venix/hookla/App.scala index 20b48cf..af2cc35 100644 --- a/src/main/scala/venix/hookla/App.scala +++ b/src/main/scala/venix/hookla/App.scala @@ -8,12 +8,13 @@ import io.getquill.context.zio._ import io.getquill.util.LoadConfig import sttp.client3.httpclient.zio.HttpClientZioBackend import sttp.tapir.json.circe._ -import venix.hookla.RequestError.Unauthenticated +import venix.hookla.RequestError.{BadRequest, Forbidden, Unauthenticated} import venix.hookla.http.Auth import venix.hookla.resolvers._ import venix.hookla.services.core._ import venix.hookla.services.db._ import venix.hookla.services.http.DiscordUserService +import venix.hookla.sources.WebhookController import zio._ import zio.http._ import zio.logging.backend.SLF4J @@ -30,17 +31,23 @@ object App extends ZIOAppDefault { migrationService <- ZIO.service[FlywayMigrationService] // _ <- migrationService.migrate().orDie - schemaResolver <- ZIO.service[SchemaResolver] + webhookController <- ZIO.service[WebhookController] + schemaResolver <- ZIO.service[SchemaResolver] api = schemaResolver.graphQL apiInterpreter <- api.interpreter app = Http - .collectHttp[Request] { case _ -> !! / "api" / "graphql" => - ZHttpAdapter.makeHttpService(HttpInterpreter(apiInterpreter)) @@ Auth.middleware + .collectHttp[Request] { + case Method.POST -> !! / "api" / "v1" / "webhooks" / webhookId => + webhookController.makeHttpService + case _ -> !! / "api" / "graphql" => + ZHttpAdapter.makeHttpService(HttpInterpreter(apiInterpreter)) @@ Auth.middleware } .tapErrorCauseZIO(cause => ZIO.logErrorCause(cause)) .mapError { case e: Unauthenticated => Response(status = Status.Unauthorized, body = Body.fromString(Json.obj("error" -> Json.fromString(e.message)).spaces2)) + case e: Forbidden => Response(status = Status.Forbidden, body = Body.fromString(Json.obj("error" -> Json.fromString(e.message)).spaces2)) + case e: BadRequest => Response(status = Status.BadRequest, body = Body.fromString(Json.obj("error" -> Json.fromString(e.message)).spaces2)) case _ => Response(status = Status.InternalServerError) } @@ -81,6 +88,7 @@ object App extends ZIOAppDefault { HookResolver.live, HookService.live, TeamResolver.live, + WebhookController.live, // zhttp server config Server.defaultWithPort(8443), logger diff --git a/src/main/scala/venix/hookla/package.scala b/src/main/scala/venix/hookla/package.scala index 9d9f1e8..6af251e 100644 --- a/src/main/scala/venix/hookla/package.scala +++ b/src/main/scala/venix/hookla/package.scala @@ -9,6 +9,7 @@ import venix.hookla.resolvers._ import venix.hookla.services.core.{AuthService, HTTPService} import venix.hookla.services.db.{FlywayMigrationService, HookService, UserService} import venix.hookla.services.http.DiscordUserService +import venix.hookla.sources.WebhookController import venix.hookla.types.RichNewtype import zio._ import zio.http.Server @@ -21,7 +22,7 @@ package object hookla { object QuillContext extends PostgresZioJAsyncContext(SnakeCase) - type Env = HooklaConfig with ZioJAsyncConnection with Redis with SttpClient with Auth with UserResolver with HookResolver with HTTPService with FlywayMigrationService with DiscordUserService with SinkResolver with SourceResolver with SchemaResolver with UserResolver with UserService with HookService with AuthService with Server + type Env = HooklaConfig with ZioJAsyncConnection with Redis with SttpClient with Auth with UserResolver with HookResolver with HTTPService with FlywayMigrationService with DiscordUserService with SinkResolver with SourceResolver with SchemaResolver with UserResolver with UserService with WebhookController with HookService with AuthService with Server type Result[T] = IO[RequestError, T] type ResultOpt[T] = IO[RequestError, Option[T]] diff --git a/src/main/scala/venix/hookla/services/db/HookService.scala b/src/main/scala/venix/hookla/services/db/HookService.scala index 9f3aa94..690366d 100644 --- a/src/main/scala/venix/hookla/services/db/HookService.scala +++ b/src/main/scala/venix/hookla/services/db/HookService.scala @@ -8,6 +8,8 @@ import zio.ZLayer trait HookService extends BaseDBService { def get(team: TeamId, hook: HookId): Result[Option[Hook]] + def get(id: HookId): Result[Option[Hook]] + // Should only be used when resolving something you know 100% exists because of SQL constraints def getUnsafe(hook: HookId): Result[Hook] def getByTeam(team: TeamId): Result[List[Hook]] @@ -25,6 +27,14 @@ private class HookServiceImpl(private val ctx: ZioJAsyncConnection) extends Hook .mapBoth(DatabaseError, _.headOption) .provide(ZLayer.succeed(ctx)) + def get(id: HookId) = + run { + hooks + .filter(_.id == lift(id)) + } + .mapBoth(DatabaseError, _.headOption) + .provide(ZLayer.succeed(ctx)) + def getUnsafe(hook: HookId) = run { hooks diff --git a/src/main/scala/venix/hookla/sources/WebhookController.scala b/src/main/scala/venix/hookla/sources/WebhookController.scala new file mode 100644 index 0000000..53b9a71 --- /dev/null +++ b/src/main/scala/venix/hookla/sources/WebhookController.scala @@ -0,0 +1,63 @@ +package venix.hookla.sources + +import io.circe.Json +import sttp.tapir.{Endpoint, PublicEndpoint} +import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions} +import venix.hookla.Env +import venix.hookla.RequestError.BadRequest +import venix.hookla.services.db.HookService +import venix.hookla.types.HookId +import zio.http.{Body, HttpApp, Request, Response, Status} +import zio.{&, RIO, ZIO, ZLayer} +import sttp.tapir.ztapir._ + +import java.util.UUID + +trait WebhookController { + def makeHttpService[R](implicit serverOptions: ZioHttpServerOptions[R] = ZioHttpServerOptions.default[R]): HttpApp[R & Env, Throwable] +} + +/** + * This class contains the handling of the webhooks that are INCOMING from sources + * such as GitHub, GitLab, BitBucket, Sonarr, Radarr, etc.. + */ +private class WebhookControllerImpl( + private val hookService: HookService +) extends WebhookController { + // URI: /api/v1/handle/:hookId + // Method: POST + private def handleWebhook(request: Request): ZIO[Env, String, String] = (for { + _ <- ZIO.unit // just to start the for comprehension + + maybeHookId = request.url.path.dropTrailingSlash.last + _ <- ZIO.fail(BadRequest("You need to pass a webhook ID.")) when maybeHookId.isEmpty + + // TODO: This is a bit ugly, but it works for now. + hookId <- ZIO.attempt(UUID.fromString(maybeHookId.get)).map(HookId(_)) + + hook <- hookService.get(hookId) + _ <- ZIO.fail(BadRequest("Invalid webhook ID.")) when hook.isEmpty + // TODO: Update last used timestamp + + handler <- SourceHandler.getHandlerById(hook.get.sourceId) + eventHandler = handler.determineEvent(request) + + _ <- eventHandler.handle(request, hook.get) + } yield Json.obj("message" -> Json.fromString("")).spaces2).orElseFail("temp") // TODO: Fix out how to have better errors here. + + private def webhookEndpoint = endpoint + .in(extractFromRequest[Request](x => x.underlying.asInstanceOf[Request])) + .errorOut(stringBody) + .out(stringJsonBody) + + def makeHttpService[R](implicit serverOptions: ZioHttpServerOptions[R]): HttpApp[R & Env, Throwable] = + ZioHttpInterpreter(serverOptions) + .toHttp(webhookEndpoint.zServerLogic(handleWebhook)) +} + +object WebhookController { + private type In = HookService + private def create(hookService: HookService) = new WebhookControllerImpl(hookService) + + val live: zio.ZLayer[In, Throwable, WebhookController] = ZLayer.fromFunction(create _) +} From 3c5db0aea15aaa2095abe5e025180cf14c4f8d4f Mon Sep 17 00:00:00 2001 From: Daave Date: Fri, 18 Aug 2023 23:04:27 +0100 Subject: [PATCH 5/6] feat: Basic stubbed flow for GH push event --- .../venix/hookla/sources/SourceHandler.scala | 8 +++++--- .../venix/hookla/sources/WebhookController.scala | 6 +++--- .../sources/github/GithubSourceHandler.scala | 15 +++++++++++++++ .../hookla/sources/github/events/PushEvent.scala | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala create mode 100644 src/main/scala/venix/hookla/sources/github/events/PushEvent.scala diff --git a/src/main/scala/venix/hookla/sources/SourceHandler.scala b/src/main/scala/venix/hookla/sources/SourceHandler.scala index 7e1c8e9..fb75825 100644 --- a/src/main/scala/venix/hookla/sources/SourceHandler.scala +++ b/src/main/scala/venix/hookla/sources/SourceHandler.scala @@ -1,9 +1,11 @@ package venix.hookla.sources +import venix.hookla.Result +import venix.hookla.sources.github.GithubSourceHandler import zio.{URIO, ZIO} import zio.http.Request -trait SourceHandler { +private[sources] trait SourceHandler { /* * This method is called when a webhook is received. * It should determine the event type and then call the appropriate method. @@ -11,12 +13,12 @@ trait SourceHandler { * The request is passed in so that the handler can determine the event type. * i.e. push, issue, deployment, etc... */ - def determineEvent(req: Request): SourceEventHandler + def determineEvent(req: Request): Result[SourceEventHandler] } object SourceHandler { def getHandlerById(id: String): URIO[Any, SourceHandler] = id match { - case "github" => ??? + case "github" => ZIO.succeed(GithubSourceHandler) } } diff --git a/src/main/scala/venix/hookla/sources/WebhookController.scala b/src/main/scala/venix/hookla/sources/WebhookController.scala index 53b9a71..5b32316 100644 --- a/src/main/scala/venix/hookla/sources/WebhookController.scala +++ b/src/main/scala/venix/hookla/sources/WebhookController.scala @@ -39,11 +39,11 @@ private class WebhookControllerImpl( _ <- ZIO.fail(BadRequest("Invalid webhook ID.")) when hook.isEmpty // TODO: Update last used timestamp - handler <- SourceHandler.getHandlerById(hook.get.sourceId) - eventHandler = handler.determineEvent(request) + handler <- SourceHandler.getHandlerById(hook.get.sourceId) + eventHandler <- handler.determineEvent(request) _ <- eventHandler.handle(request, hook.get) - } yield Json.obj("message" -> Json.fromString("")).spaces2).orElseFail("temp") // TODO: Fix out how to have better errors here. + } yield Json.obj("message" -> Json.fromString("Success!")).spaces2).mapError { e => println(e); "temp" } // TODO: Figure out how to have better errors here. private def webhookEndpoint = endpoint .in(extractFromRequest[Request](x => x.underlying.asInstanceOf[Request])) diff --git a/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala b/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala new file mode 100644 index 0000000..7b31fc6 --- /dev/null +++ b/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala @@ -0,0 +1,15 @@ +package venix.hookla.sources.github + +import venix.hookla.Result +import venix.hookla.sources.{SourceEventHandler, SourceHandler} +import zio.ZIO +import zio.http.Request + +case object GithubSourceHandler extends SourceHandler { + private val eventMap: Map[String, SourceEventHandler] = Map( + "push" -> events.PushEvent + ) + + override def determineEvent(req: Request): Result[SourceEventHandler] = + ZIO.attempt(eventMap(req.headers.get("X-GitHub-Event").get.toLowerCase)).orDie // TODO: Handle this properly +} diff --git a/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala b/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala new file mode 100644 index 0000000..b1a810e --- /dev/null +++ b/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala @@ -0,0 +1,15 @@ +package venix.hookla.sources.github.events + +import venix.hookla.Task +import venix.hookla.models.Hook +import venix.hookla.sources.SourceEventHandler +import zio.ZIO +import zio.http.Request + +private[github] case object PushEvent extends SourceEventHandler { + override def handle(request: Request, hook: Hook): Task[Unit] = { + println("hello there") + + ZIO.unit + } +} From af4dbb3b1cfe1067b301e762d0843bf96329e102 Mon Sep 17 00:00:00 2001 From: Daave Date: Mon, 2 Oct 2023 22:07:25 +0100 Subject: [PATCH 6/6] feat: Basic working flow --- .../services/http/DiscordWebhookService.scala | 23 +++++++++++++++++++ .../hookla/sources/SourceEventHandler.scala | 16 ++++++++++--- .../venix/hookla/sources/SourceHandler.scala | 6 ++--- .../hookla/sources/WebhookController.scala | 13 ++++++++--- .../sources/github/GithubSourceHandler.scala | 9 ++++---- .../sources/github/events/PingEvent.scala | 15 ++++++++++++ .../sources/github/events/PushEvent.scala | 7 +++--- 7 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 src/main/scala/venix/hookla/services/http/DiscordWebhookService.scala create mode 100644 src/main/scala/venix/hookla/sources/github/events/PingEvent.scala diff --git a/src/main/scala/venix/hookla/services/http/DiscordWebhookService.scala b/src/main/scala/venix/hookla/services/http/DiscordWebhookService.scala new file mode 100644 index 0000000..e9c6dc1 --- /dev/null +++ b/src/main/scala/venix/hookla/services/http/DiscordWebhookService.scala @@ -0,0 +1,23 @@ +package venix.hookla.services.http + +import io.circe.Codec +import io.circe.generic.semiauto._ +import sttp.client3.UriContext +import venix.hookla.{HooklaConfig, Result} +import venix.hookla.services.core.{HTTPService, Options} +import zio.ZLayer + +trait DiscordWebhookService { + +} + +private class DiscordWebhookServiceImpl(private val http: HTTPService, private val config: HooklaConfig) extends DiscordWebhookService { + def execute(id: String): Result[Option[DiscordUser]] = http.post[Option[DiscordUser]](uri"https://canary.discord.com/api/webhooks/689887952268165178/J6GACLSgtVdOKO_tP3CWVmy_PV3_3A6T8Pc2lL1b0ZUHCviVQlhk31ElB7_vJA7w_rIK", Options().addHeader("Authorization", s"Bot ${config.discord.token}")) +} + +object DiscordWebhookService { + private type In = HTTPService with HooklaConfig + private def create(httpService: HTTPService, c: HooklaConfig) = new DiscordWebhookServiceImpl(httpService, c) + + val live: ZLayer[In, Throwable, DiscordWebhookService] = ZLayer.fromFunction(create _) +} diff --git a/src/main/scala/venix/hookla/sources/SourceEventHandler.scala b/src/main/scala/venix/hookla/sources/SourceEventHandler.scala index 8b4465a..856d3df 100644 --- a/src/main/scala/venix/hookla/sources/SourceEventHandler.scala +++ b/src/main/scala/venix/hookla/sources/SourceEventHandler.scala @@ -1,9 +1,19 @@ package venix.hookla.sources +import io.circe.Json import venix.hookla.Task import venix.hookla.models.Hook -import zio.http.Request -trait SourceEventHandler { - def handle(request: Request, hook: Hook): Task[Unit] +/** + * This trait is used to handle the body of a webhook, after the event type has been determined. + * The reason there is a type argument is because different sources might use different formats i.e. JSON, XML, etc... + * + * @tparam T The type of the body of the webhook (i.e. Json, String, case class, etc...) + */ +sealed trait SourceEventHandler[T <: Serializable] { + def handle(body: T, headers: Map[String, String], hook: Hook): Task[Unit] +} + +trait GithubSourceEventHandler extends SourceEventHandler[Json] { + def handle(body: Json, headers: Map[String, String], hook: Hook): Task[Unit] } diff --git a/src/main/scala/venix/hookla/sources/SourceHandler.scala b/src/main/scala/venix/hookla/sources/SourceHandler.scala index fb75825..521b136 100644 --- a/src/main/scala/venix/hookla/sources/SourceHandler.scala +++ b/src/main/scala/venix/hookla/sources/SourceHandler.scala @@ -2,7 +2,7 @@ package venix.hookla.sources import venix.hookla.Result import venix.hookla.sources.github.GithubSourceHandler -import zio.{URIO, ZIO} +import zio.{UIO, URIO, ZIO} import zio.http.Request private[sources] trait SourceHandler { @@ -13,11 +13,11 @@ private[sources] trait SourceHandler { * The request is passed in so that the handler can determine the event type. * i.e. push, issue, deployment, etc... */ - def determineEvent(req: Request): Result[SourceEventHandler] + def determineEvent(req: Request): Result[SourceEventHandler[_ <: Serializable]] } object SourceHandler { - def getHandlerById(id: String): URIO[Any, SourceHandler] = + def getHandlerById(id: String): UIO[SourceHandler] = id match { case "github" => ZIO.succeed(GithubSourceHandler) } diff --git a/src/main/scala/venix/hookla/sources/WebhookController.scala b/src/main/scala/venix/hookla/sources/WebhookController.scala index 5b32316..fe2cd2b 100644 --- a/src/main/scala/venix/hookla/sources/WebhookController.scala +++ b/src/main/scala/venix/hookla/sources/WebhookController.scala @@ -1,6 +1,7 @@ package venix.hookla.sources import io.circe.Json +import io.circe.syntax._ import sttp.tapir.{Endpoint, PublicEndpoint} import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions} import venix.hookla.Env @@ -26,7 +27,7 @@ private class WebhookControllerImpl( ) extends WebhookController { // URI: /api/v1/handle/:hookId // Method: POST - private def handleWebhook(request: Request): ZIO[Env, String, String] = (for { + private def handleWebhook(request: Request, body: String): ZIO[Env, String, String] = (for { _ <- ZIO.unit // just to start the for comprehension maybeHookId = request.url.path.dropTrailingSlash.last @@ -42,17 +43,23 @@ private class WebhookControllerImpl( handler <- SourceHandler.getHandlerById(hook.get.sourceId) eventHandler <- handler.determineEvent(request) - _ <- eventHandler.handle(request, hook.get) + // TODO: This needs to be abstracted out to support non-JSON body's like the handler traits have. + jsonBody <- ZIO.attempt(body.asJson) + + _ <- eventHandler + .asInstanceOf[GithubSourceEventHandler] + .handle(jsonBody, request.headers.map(x => x.headerName -> x.renderedValue).toMap, hook.get) } yield Json.obj("message" -> Json.fromString("Success!")).spaces2).mapError { e => println(e); "temp" } // TODO: Figure out how to have better errors here. private def webhookEndpoint = endpoint .in(extractFromRequest[Request](x => x.underlying.asInstanceOf[Request])) + .in(stringJsonBody) .errorOut(stringBody) .out(stringJsonBody) def makeHttpService[R](implicit serverOptions: ZioHttpServerOptions[R]): HttpApp[R & Env, Throwable] = ZioHttpInterpreter(serverOptions) - .toHttp(webhookEndpoint.zServerLogic(handleWebhook)) + .toHttp(webhookEndpoint.zServerLogic(c => handleWebhook(c._1, c._2))) } object WebhookController { diff --git a/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala b/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala index 7b31fc6..af83071 100644 --- a/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala +++ b/src/main/scala/venix/hookla/sources/github/GithubSourceHandler.scala @@ -1,15 +1,16 @@ package venix.hookla.sources.github import venix.hookla.Result -import venix.hookla.sources.{SourceEventHandler, SourceHandler} +import venix.hookla.sources.{GithubSourceEventHandler, SourceHandler} import zio.ZIO import zio.http.Request case object GithubSourceHandler extends SourceHandler { - private val eventMap: Map[String, SourceEventHandler] = Map( - "push" -> events.PushEvent + private val eventMap: Map[String, GithubSourceEventHandler] = Map( + "push" -> events.PushEvent, + "ping" -> events.PingEvent ) - override def determineEvent(req: Request): Result[SourceEventHandler] = + override def determineEvent(req: Request): Result[GithubSourceEventHandler] = ZIO.attempt(eventMap(req.headers.get("X-GitHub-Event").get.toLowerCase)).orDie // TODO: Handle this properly } diff --git a/src/main/scala/venix/hookla/sources/github/events/PingEvent.scala b/src/main/scala/venix/hookla/sources/github/events/PingEvent.scala new file mode 100644 index 0000000..6e21f01 --- /dev/null +++ b/src/main/scala/venix/hookla/sources/github/events/PingEvent.scala @@ -0,0 +1,15 @@ +package venix.hookla.sources.github.events + +import io.circe.Json +import venix.hookla.Task +import venix.hookla.models.Hook +import venix.hookla.sources.GithubSourceEventHandler +import zio.ZIO + +private[github] case object PingEvent extends GithubSourceEventHandler { + def handle(body: Json, headers: Map[String, String], hook: Hook): Task[Unit] = { + println("hello there") + + ZIO.unit + } +} diff --git a/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala b/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala index b1a810e..c8a2fb2 100644 --- a/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala +++ b/src/main/scala/venix/hookla/sources/github/events/PushEvent.scala @@ -1,13 +1,14 @@ package venix.hookla.sources.github.events +import io.circe.Json import venix.hookla.Task import venix.hookla.models.Hook -import venix.hookla.sources.SourceEventHandler +import venix.hookla.sources.{GithubSourceEventHandler, SourceEventHandler} import zio.ZIO import zio.http.Request -private[github] case object PushEvent extends SourceEventHandler { - override def handle(request: Request, hook: Hook): Task[Unit] = { +private[github] case object PushEvent extends GithubSourceEventHandler { + def handle(body: Json, headers: Map[String, String], hook: Hook): Task[Unit] = { println("hello there") ZIO.unit