Skip to content

Commit

Permalink
Feedback on slack (#5)
Browse files Browse the repository at this point in the history
Sending feedback to slack
  • Loading branch information
otrebski authored Sep 17, 2020
1 parent e135520 commit 80ffdf7
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 49 deletions.
89 changes: 49 additions & 40 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,27 @@ lazy val quizz =
.settings(settings)
.settings(
libraryDependencies ++= Seq(
library.scalaCheck % Test,
library.scalaTest % Test,
library.circeParser,
library.circeGeneric,
library.tapir,
library.tapirAkka,
library.tapirJson,
library.scalaLogging,
library.logback,
library.doobieCore,
library.doobiePostgres,
library.doobieQuill,
library.doobieScalatest % Test,
library.scalaCheck % Test,
library.scalaTest % Test,
library.circeParser,
library.circeGeneric,
library.tapir,
library.tapirAkka,
library.tapirJson,
library.sttpClient,
library.sttpClientCirce,
library.tapirJsonCirce,
library.scalaLogging,
library.logback,
library.doobieCore,
library.doobiePostgres,
library.doobieQuill,
library.doobieScalatest % Test
// library.tapirHttp4s,
// library.tapirJson,
// library.bazelServer,
// library.bazelClient
)
)
)

// *****************************************************************************
Expand All @@ -41,29 +44,34 @@ lazy val library =
object Version {
val scalaCheck = "1.14.1"
val scalaTest = "3.2.0"
val circe = "0.12.3"
val tapir = "0.11.11"
val circe = "0.12.3"
val tapir = "0.11.11"
// val bazel = "0.20.0"
val scalaLogging = "3.9.2"
val logback = "1.2.3"
val doobie = "0.9.0"
val logback = "1.2.3"
val doobie = "0.9.0"
val sttp = "2.2.8"
}
val scalaCheck = "org.scalacheck" %% "scalacheck" % Version.scalaCheck
val scalaTest = "org.scalatest" %% "scalatest" % Version.scalaTest
val circeParser = "io.circe" %% "circe-parser" % Version.circe
val circeGeneric = "io.circe" %% "circe-generic" % Version.circe
val tapir = "com.softwaremill.tapir" %% "tapir-core" % Version.tapir
val tapirAkka = "com.softwaremill.tapir" %% "tapir-akka-http-server" % Version.tapir
val tapirJson = "com.softwaremill.tapir" %% "tapir-json-circe" % Version.tapir
val scalaCheck = "org.scalacheck" %% "scalacheck" % Version.scalaCheck
val scalaTest = "org.scalatest" %% "scalatest" % Version.scalaTest
val circeParser = "io.circe" %% "circe-parser" % Version.circe
val circeGeneric = "io.circe" %% "circe-generic" % Version.circe
val tapir = "com.softwaremill.tapir" %% "tapir-core" % Version.tapir
val tapirAkka = "com.softwaremill.tapir" %% "tapir-akka-http-server" % Version.tapir
val tapirJson = "com.softwaremill.tapir" %% "tapir-json-circe" % Version.tapir
// val bazelClient = "org.http4s" %% "http4s-blaze-client" % Version.bazel
// val bazelServer = "org.http4s" %% "http4s-blaze-server" % Version.bazel
// val tapirHttp4s = "com.softwaremill.tapir" %% "tapir-http4s-server" % Version.tapir
val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % Version.scalaLogging
val logback = "ch.qos.logback" % "logback-classic" % Version.logback
val doobieCore = "org.tpolecat" %% "doobie-core" % Version.doobie
val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % Version.doobie
val doobieQuill = "org.tpolecat" %% "doobie-quill" % Version.doobie
val doobieScalatest = "org.tpolecat" %% "doobie-scalatest" % Version.doobie
val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % Version.scalaLogging
val sttpClient = "com.softwaremill.sttp.client" %% "core" % Version.sttp
val sttpClientCirce = "com.softwaremill.sttp.client" %% "circe" % Version.sttp
val tapirJsonCirce = "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % Version.tapir

val logback = "ch.qos.logback" % "logback-classic" % Version.logback
val doobieCore = "org.tpolecat" %% "doobie-core" % Version.doobie
val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % Version.doobie
val doobieQuill = "org.tpolecat" %% "doobie-quill" % Version.doobie
val doobieScalatest = "org.tpolecat" %% "doobie-scalatest" % Version.doobie
}

// *****************************************************************************
Expand All @@ -82,19 +90,20 @@ lazy val commonSettings =
startYear := Some(2019),
licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")),
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-language:_",
"-target:jvm-1.8",
"-encoding", "UTF-8",
),
"-unchecked",
"-deprecation",
"-language:_",
"-target:jvm-1.8",
"-encoding",
"UTF-8"
),
Compile / unmanagedSourceDirectories := Seq((Compile / scalaSource).value),
Test / unmanagedSourceDirectories := Seq((Test / scalaSource).value),
)
Test / unmanagedSourceDirectories := Seq((Test / scalaSource).value)
)

lazy val scalafmtSettings =
Seq(
scalafmtOnCompile := true,
scalafmtOnCompile := true
)

mainClass := Some("quizz.web.WebApp")
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ services:
- '/tmp/mindmups/:/tmp/mindmups/'
environment:
LOAD_FROM_DIR: "/tmp/mindmups/"
USE_SLACK: "false"
SLACK_TOKEN: ""

gui:
image: otrebski/quizz-gui:latest
Expand Down
2 changes: 0 additions & 2 deletions gui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ RUN apk add npm
RUN npm --version
COPY ./gui/ /workdir
WORKDIR /workdir
RUN ls -la
RUN npm install && npm run-script build
RUN ls -la build

FROM nginx
COPY ./gui/docker/default.conf /etc/nginx/conf.d/
Expand Down
Binary file modified gui/public/favicon.ico
Binary file not shown.
9 changes: 9 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@ quizz {
dir = ""
dir = ${?LOAD_FROM_DIR}
}
}

feedback {
slack {
use = false
use = ${?USE_SLACK}
token = ""
token = ${?SLACK_TOKEN}
}
}
101 changes: 101 additions & 0 deletions src/main/scala/quizz/feedback/FeedbackSender.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package quizz.feedback

import cats.effect.Sync
import cats.syntax.option._
import com.typesafe.scalalogging.LazyLogging
import quizz.web.WebApp.Api.{ Feedback, QuizzState }

import scala.language.higherKinds

trait FeedbackSender[F[_]] {

def send(feedback: Feedback, quizzState: QuizzState): F[Unit]
}

class LogFeedbackSender[F[_]: Sync] extends FeedbackSender[F] with LazyLogging {
override def send(feedback: Feedback, quizzState: QuizzState): F[Unit] =
Sync[F].delay(logger.info(s"Have feedback: $feedback for $quizzState"))
}

object SlackFeedbackSender {
case class SlackMessage(blocks: List[Block])
case class Block(text: Text, `type`: String = "section", block_id: Option[String] = None)
case class Text(text: String, `type`: String = "mrkdwn")

private def feedbackIcon(feedback: Feedback): String =
feedback.rate match {
case a if a > 0 => ":+1:"
case a if a < 0 => ":-1:"
case a if a == 0 => ":point_right:"
}

def convertFeedback(feedback: Feedback, quizzState: QuizzState): SlackMessage = {
val history: List[Block] = quizzState.history.foldRight(List.empty[Block]) {
(historyStep, list) =>
val answers = historyStep.answers
.map(answer =>
s" - ${answer.selected.map(if (_) ":ballot_box_with_check:" else ":black_square_button:").getOrElse(":black_square_button:")} ${answer.text}"
)
.mkString("\n")
Block(
Text(
s"${historyStep.question}\n$answers"
),
block_id = historyStep.id.some
) :: list
}
val answers = quizzState.currentStep.answers
.map(answer =>
s" - ${answer.selected.map(if (_) ":heavy_check_mark:" else "").getOrElse("")} ${answer.text}"
)
.mkString("\n")
val lastQuestion = Block(
Text(
s"${quizzState.currentStep.question}\n$answers"
),
block_id = quizzState.currentStep.id.some
)
val header = Block(
Text(
s"Feedback ${feedbackIcon(feedback)} for quiz ${feedback.quizzId}",
`type` = "plain_text"
),
block_id = "header".some,
`type` = "header"
)
val comment = Block(
Text(s"Comment: ${feedback.comment}"),
block_id = "comment".some
)
SlackMessage(header :: comment :: history ::: List(lastQuestion))
}
}

class SlackFeedbackSender[F[_]: Sync](token: String) extends FeedbackSender[F] {

override def send(feedback: Feedback, quizzState: QuizzState): F[Unit] =
Sync[F].delay {
import sttp.client.circe._

val url: String = s"https://hooks.slack.com/services/$token"
import sttp.client._
val uri = uri"$url"
val rate = feedback.rate match {
case a if a > 0 => ":+1:"
case a if a < 0 => ":-1:"
case a if a == 0 => ":point_right:"
}

val message = SlackFeedbackSender.convertFeedback(feedback, quizzState)
import io.circe.generic.auto._

val myRequest = basicRequest
.post(uri)
.header("Content-type", "application/json")
.body(message)

implicit val backend: SttpBackend[Identity, Nothing, NothingT] = HttpURLConnectionBackend()
val response: Identity[Response[Either[String, String]]] = myRequest.send()
response
}
}
33 changes: 26 additions & 7 deletions src/main/scala/quizz/web/WebApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import com.typesafe.config.{ Config, ConfigFactory }
import com.typesafe.scalalogging.LazyLogging
import io.circe
import io.circe.generic.auto._
import mindmup.Parser
import quizz.data.{ ExamplesData, Loader }
import quizz.feedback.{ LogFeedbackSender, SlackFeedbackSender }
import quizz.model.Quizz
import quizz.web.WebApp.Api.{ AddQuizzResponse, FeedbackResponse, Quizzes }
import tapir.json.circe._
Expand Down Expand Up @@ -164,19 +164,38 @@ object WebApp extends IOApp with LazyLogging {
Parser.parseInput(request.mindmupSource).map(_.toQuizz.copy(id = request.id))

newQuizzOrError match {
case Left(error) => Future.failed(new Exception(error.toString))
case Left(error) => Future.failed(new Exception(error.toString))
case Right(quizz) =>
// logger.info(s"Adding quizz $quizz")
val io: IO[Either[String, Map[String, Quizz]]] =
quizzes.getAndUpdate(old => old.map(_.updated(request.id, quizz)))
io.map(_ => AddQuizzResponse("Added").asRight)
.unsafeToFuture()
}
}

def feedbackProvider(feedback: Api.Feedback): Future[Either[Unit, FeedbackResponse]] = {
logger.info(s"Have feedback: $feedback")
Future.successful(Right(FeedbackResponse("OK")))
def feedbackProvider(
quizzes: Ref[IO, Either[String, Map[String, Quizz]]]
)(feedback: Api.Feedback): Future[Either[Unit, FeedbackResponse]] = {
val log = new LogFeedbackSender[IO]
val useSlack = config.getBoolean("feedback.slack.use")
val request = Api.QuizzQuery(feedback.quizzId, feedback.path)
val quizzState: IO[Either[String, Api.QuizzState]] = for {
q <- quizzes.get
result = q.flatMap(quizzes => Logic.calculateStateOnPath(request, quizzes))
} yield result

val p = quizzState.flatMap {
case Right(quizzState) =>
if (useSlack) {
val slack = new SlackFeedbackSender[IO](config.getString("feedback.slack.token"))
log.send(feedback, quizzState).flatMap(_ => slack.send(feedback, quizzState))
} else
log.send(feedback, quizzState)
case Left(error) => IO.raiseError(new Exception(s"Can't process feedback: $error"))
}

implicit val ec: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global
p.unsafeToFuture().map { _ => Right(FeedbackResponse("OK")) }
}

override def run(args: List[String]): IO[ExitCode] = {
Expand All @@ -191,7 +210,7 @@ object WebApp extends IOApp with LazyLogging {
val route = routeEndpoint.toRoute(routeWithPathProvider(quizzes))
val routeStart = routeEndpointStart.toRoute(routeWithoutPathProvider(quizzes))
val routeList = listQuizzes.toRoute(quizListProvider(quizzes))
val routeFeedback = feedback.toRoute(feedbackProvider)
val routeFeedback = feedback.toRoute(feedbackProvider(quizzes))
val add = addQuizz.toRoute(addQuizzProvider(quizzes))
implicit val system: ActorSystem = ActorSystem("my-system")
implicit val materializer: ActorMaterializer = ActorMaterializer()
Expand Down

0 comments on commit 80ffdf7

Please sign in to comment.