diff --git a/.idea/modules/delivery-checker-bot-build.iml b/.idea/modules/delivery-checker-bot-build.iml
new file mode 100644
index 0000000..19ecb3a
--- /dev/null
+++ b/.idea/modules/delivery-checker-bot-build.iml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/delivery-checker-bot.iml b/.idea/modules/delivery-checker-bot.iml
new file mode 100644
index 0000000..899bff0
--- /dev/null
+++ b/.idea/modules/delivery-checker-bot.iml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/root-build.iml b/.idea/modules/root-build.iml
deleted file mode 100644
index 4c662cd..0000000
--- a/.idea/modules/root-build.iml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/root.iml b/.idea/modules/root.iml
deleted file mode 100644
index a232ccc..0000000
--- a/.idea/modules/root.iml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/telegram-bot.iml b/.idea/modules/telegram-bot.iml
new file mode 100644
index 0000000..747efe4
--- /dev/null
+++ b/.idea/modules/telegram-bot.iml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/telegram-bot1.iml b/.idea/modules/telegram-bot1.iml
new file mode 100644
index 0000000..5c2e88b
--- /dev/null
+++ b/.idea/modules/telegram-bot1.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
index 5d77978..1c35b5a 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,21 +1,37 @@
import Dependencies._
-lazy val root = (project in file(".")).
- settings(
- inThisBuild(List(
- organization := "com.example",
- scalaVersion := "2.12.6",
- version := "0.1.0-SNAPSHOT",
- mainClass := Some("eu.xeppaka.bot1.TelegramBotServer")
- )),
- name := "telegram-bot1",
+lazy val commonSettings = Seq(
+ organization := "com.example",
+ scalaVersion := "2.12.7",
+ version := "0.1.0-SNAPSHOT",
+ mainClass := Some("eu.xeppaka.bot.TelegramBotServer")
+)
+
+inThisBuild(commonSettings)
+
+lazy val `telegram-bot` = (project in file("telegram-bot"))
+ .settings(
+ libraryDependencies ++= Seq(
+ scalaTest % Test,
+ akka,
+ akkaTyped,
+ akkaHttp,
+ akkaStream,
+ circleCore,
+ circleGeneric,
+ circleParser,
+ circeAkkaHttp
+ )
+ )
+
+lazy val `delivery-checker-bot` = (project in file("delivery-checker-bot"))
+ .settings(
libraryDependencies ++= Seq(
scalaTest % Test,
akka,
akkaTyped,
akkaHttp,
akkaStream,
- vkapi,
circleCore,
circleGeneric,
circleParser
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index c741389..ab1c894 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -1,13 +1,14 @@
import sbt._
object Dependencies {
+ lazy val akka = "com.typesafe.akka" %% "akka-actor" % "2.5.17"
+ lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % "2.5.17"
+ lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % "2.5.17"
+ lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.5"
+ //lazy val vkapi = "com.vk.api" % "sdk" % "0.5.12"
+ lazy val circleCore = "io.circe" %% "circe-core" % "0.10.0"
+ lazy val circleGeneric = "io.circe" %% "circe-generic" % "0.10.0"
+ lazy val circleParser = "io.circe" %% "circe-parser" % "0.10.0"
+ lazy val circeAkkaHttp = "de.heikoseeberger" %% "akka-http-circe" % "1.22.0"
lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5"
- lazy val akka = "com.typesafe.akka" %% "akka-actor" % "2.5.12"
- lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % "2.5.12"
- lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.1"
- lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % "2.5.12"
- lazy val vkapi = "com.vk.api" % "sdk" % "0.5.12"
- lazy val circleCore = "io.circe" %% "circe-core" % "0.9.3"
- lazy val circleGeneric = "io.circe" %% "circe-generic" % "0.9.3"
- lazy val circleParser = "io.circe" %% "circe-parser" % "0.9.3"
}
diff --git a/project/build.properties b/project/build.properties
index 64cf32f..7c58a83 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.1.4
+sbt.version=1.2.6
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
deleted file mode 100644
index 8ea5900..0000000
--- a/src/main/resources/application.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-akka {
- loglevel = "INFO"
-}
\ No newline at end of file
diff --git a/src/main/resources/telegram-bot.p12 b/src/main/resources/telegram-bot.p12
deleted file mode 100644
index 8147227..0000000
Binary files a/src/main/resources/telegram-bot.p12 and /dev/null differ
diff --git a/src/main/resources/telegram-bot.pem b/src/main/resources/telegram-bot.pem
deleted file mode 100644
index 0fe289b..0000000
--- a/src/main/resources/telegram-bot.pem
+++ /dev/null
@@ -1,21 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDcTCCAlmgAwIBAgIJAKLMfxW4KRHuMA0GCSqGSIb3DQEBCwUAME8xCzAJBgNV
-BAYTAkNaMQ8wDQYDVQQHDAZQcmFndWUxGjAYBgNVBAoMEVBhdmVsIEthY2hhbG91
-c2tpMRMwEQYDVQQDDAp4ZXBwYWthLmV1MB4XDTE4MDUxMTE4MjEzOVoXDTI4MDUw
-ODE4MjEzOVowTzELMAkGA1UEBhMCQ1oxDzANBgNVBAcMBlByYWd1ZTEaMBgGA1UE
-CgwRUGF2ZWwgS2FjaGFsb3Vza2kxEzARBgNVBAMMCnhlcHBha2EuZXUwggEiMA0G
-CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWwewslKtWJ7yeYrxDuoh5PS7y5/C/
-0NT6tsAAsh3ZVCqdeNvYj56n8jVob/jJ9EYMqKj7dXLAMopDhtuwdDN/KhW9QVkK
-zATE1wNxuV3aBVUTJuHHadUYQa7pVevvssAIa1XQ6NvU0pkwdDApylOj1TkA9MFl
-ZWHWlF0dgrVyGjFxDoWdjm2aLCdRpZCr0giTOfZ5E+OJNALTHcuJO+PRKdEreO1Y
-VAlT2Sk26f8/iG63C2/t7xWTyJKOjFPxwq3+dkNfJ1AXZ4I7aFDgP7BKogvooYuC
-BItqog+IRUOoK9Yj24KCUxD+gaI5+tv0j1ov5d0ZAqqaiSql96s2/jyZAgMBAAGj
-UDBOMB0GA1UdDgQWBBRkOXFj0c0jNdM1nJMRGr0EvfeMuTAfBgNVHSMEGDAWgBRk
-OXFj0c0jNdM1nJMRGr0EvfeMuTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
-A4IBAQBtXUOfnKdp1GY5gATTrPdr2s7FyiJvVfx/yeNNNR8ZnZcLjBMulEvXsfNi
-AL1cEctnSDyT5z2el75nTdAgTFcBZQLsVk9/4ZwBRnfunFqfU5e5X9X9z//yt4Gy
-Zq9BrMddQE+qwnOclcTDCc0GnyqKbaPiyYFcaXKhdrsflvoJI9tyLwPgjfXADLDF
-JtjC0gGdbgefDweBUMTF0cpZED9q/J2fKXHurub+3QySvUOvphVFP4dBz2WhdoTe
-v3lkEVp3I/IUv9qegO0B0o6X+Nnml4/b7HV1PArNceWOA6f57fSL2m6eN6xs4ULJ
-kfUMloAr25yvmN/tPwm+8Op5ovot
------END CERTIFICATE-----
diff --git a/src/main/scala/eu/xeppaka/bot1/CircleSupport.scala b/src/main/scala/eu/xeppaka/bot1/CircleSupport.scala
deleted file mode 100644
index 35237b2..0000000
--- a/src/main/scala/eu/xeppaka/bot1/CircleSupport.scala
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright 2015 Heiko Seeberger
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package eu.xeppaka.bot1
-
-import akka.http.scaladsl.marshalling.{ Marshaller, ToEntityMarshaller }
-import akka.http.scaladsl.model.{ ContentType, ContentTypeRange, HttpEntity }
-import akka.http.scaladsl.model.MediaType
-import akka.http.scaladsl.model.MediaTypes.`application/json`
-import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshaller }
-import akka.util.ByteString
-import cats.data.NonEmptyList
-import cats.syntax.show.toShow
-import io.circe.{ Decoder, DecodingFailure, Encoder, Json, Printer, jawn }
-import scala.collection.immutable.Seq
-
-/**
- * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol.
- * The unmarshaller fails fast, throwing the first `Error` encountered.
- *
- * To use automatic codec derivation, user needs to import `io.circe.generic.auto._`.
- */
-object FailFastCirceSupport extends FailFastCirceSupport
-
-/**
- * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol.
- * The unmarshaller fails fast, throwing the first `Error` encountered.
- *
- * To use automatic codec derivation import `io.circe.generic.auto._`.
- */
-trait FailFastCirceSupport extends BaseCirceSupport with FailFastUnmarshaller
-
-/**
- * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol.
- * The unmarshaller accumulates all errors in the exception `Errors`.
- *
- * To use automatic codec derivation, user needs to import `io.circe.generic.auto._`.
- */
-object ErrorAccumulatingCirceSupport extends ErrorAccumulatingCirceSupport {
- final case class DecodingFailures(failures: NonEmptyList[DecodingFailure]) extends Exception {
- override def getMessage = failures.toList.map(_.show).mkString("\n")
- }
-}
-
-/**
- * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol.
- * The unmarshaller accumulates all errors in the exception `Errors`.
- *
- * To use automatic codec derivation import `io.circe.generic.auto._`.
- */
-trait ErrorAccumulatingCirceSupport extends BaseCirceSupport with ErrorAccumulatingUnmarshaller
-
-/**
- * Automatic to and from JSON marshalling/unmarshalling using an in-scope circe protocol.
- */
-trait BaseCirceSupport {
-
- def unmarshallerContentTypes: Seq[ContentTypeRange] =
- mediaTypes.map(ContentTypeRange.apply)
-
- def mediaTypes: Seq[MediaType.WithFixedCharset] =
- List(`application/json`)
-
- /**
- * `Json` => HTTP entity
- *
- * @return marshaller for JSON value
- */
- implicit final def jsonMarshaller(
- implicit printer: Printer = Printer.noSpaces
- ): ToEntityMarshaller[Json] =
- Marshaller.oneOf(mediaTypes: _*) { mediaType =>
- Marshaller.withFixedContentType(ContentType(mediaType)) { json =>
- HttpEntity(mediaType, printer.pretty(json))
- }
- }
-
- /**
- * `A` => HTTP entity
- *
- * @tparam A type to encode
- * @return marshaller for any `A` value
- */
- implicit final def marshaller[A: Encoder](
- implicit printer: Printer = Printer.noSpaces
- ): ToEntityMarshaller[A] =
- jsonMarshaller(printer).compose(Encoder[A].apply)
-
- /**
- * HTTP entity => `Json`
- *
- * @return unmarshaller for `Json`
- */
- implicit final val jsonUnmarshaller: FromEntityUnmarshaller[Json] =
- Unmarshaller.byteStringUnmarshaller
- .forContentTypes(unmarshallerContentTypes: _*)
- .map {
- case ByteString.empty => throw Unmarshaller.NoContentException
- case data => jawn.parseByteBuffer(data.asByteBuffer).fold(throw _, identity)
- }
-
- /**
- * HTTP entity => `A`
- *
- * @tparam A type to decode
- * @return unmarshaller for `A`
- */
- implicit def unmarshaller[A: Decoder]: FromEntityUnmarshaller[A]
-}
-
-/**
- * Mix-in this trait to fail on the first error during unmarshalling.
- */
-trait FailFastUnmarshaller { this: BaseCirceSupport =>
-
- override implicit final def unmarshaller[A: Decoder]: FromEntityUnmarshaller[A] = {
- def decode(json: Json) = Decoder[A].decodeJson(json).fold(throw _, identity)
- jsonUnmarshaller.map(decode)
- }
-}
-
-/**
- * Mix-in this trait to accumulate all errors during unmarshalling.
- */
-trait ErrorAccumulatingUnmarshaller { this: BaseCirceSupport =>
-
- override implicit final def unmarshaller[A: Decoder]: FromEntityUnmarshaller[A] = {
- def decode(json: Json) =
- Decoder[A]
- .accumulating(json.hcursor)
- .fold(failures => throw ErrorAccumulatingCirceSupport.DecodingFailures(failures), identity)
- jsonUnmarshaller.map(decode)
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala b/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala
deleted file mode 100644
index 4a51dd2..0000000
--- a/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala
+++ /dev/null
@@ -1,175 +0,0 @@
-package eu.xeppaka.bot1
-
-import java.io.InputStream
-import java.security.{KeyStore, SecureRandom}
-import java.util.UUID
-
-import akka.actor
-import akka.actor.Scheduler
-import akka.actor.typed.scaladsl.adapter._
-import akka.actor.typed.{ActorSystem, DispatcherSelector}
-import akka.http.scaladsl.marshalling.Marshal
-import akka.http.scaladsl.model._
-import akka.http.scaladsl.server.Directives._
-import akka.http.scaladsl.server.{Route, RouteResult}
-import akka.http.scaladsl.unmarshalling.Unmarshal
-import akka.http.scaladsl.{ConnectionContext, Http, HttpExt, HttpsConnectionContext}
-import akka.stream.ActorMaterializer
-import akka.util.{ByteString, Timeout}
-import eu.xeppaka.bot1.actors.UpdateActor
-import eu.xeppaka.bot1.actors.UpdateActor.UpdateResponse
-import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
-
-import scala.collection.immutable
-import scala.concurrent.duration._
-import scala.concurrent.{ExecutionContextExecutor, Future}
-import scala.io.{Source, StdIn}
-import scala.util.{Failure, Success}
-
-class TelegramBotServer(botId: String, port: Int, httpsContext: Option[HttpsConnectionContext]) {
-
- import FailFastCirceSupport._
- import eu.xeppaka.bot1.TelegramEntities._
- import io.circe.generic.auto._
-
- private val botUri = BotUri(botId)
- private implicit val updateSystem: ActorSystem[UpdateActor.UpdateCommand] = ActorSystem(UpdateActor.behavior, "telegram-bot")
- private implicit val actorSystem: actor.ActorSystem = updateSystem.toUntyped
- private implicit val materializer: ActorMaterializer = ActorMaterializer()
- private implicit val executionContext: ExecutionContextExecutor = updateSystem.dispatchers.lookup(DispatcherSelector.default())
-
- private val http: HttpExt = Http()
- private val hookId = UUID.randomUUID().toString
- private val webhookUri = Uri(s"https://xeppaka.eu:88/$hookId")
- private val bindingFuture = http.bindAndHandle(botRoutes(hookId),
- "lenovo",
- port,
- connectionContext = httpsContext.getOrElse(http.defaultClientHttpsContext))
-
- println(s"Webhook path: $webhookUri")
- setWebhook()
-
- def stop(): Unit = {
- bindingFuture
- .flatMap(binding => deleteWebhook().map(_ => binding))
- .flatMap(binding => http.shutdownAllConnectionPools().map(_ => binding))
- .flatMap(binding => binding.unbind())
- .onComplete(_ => updateSystem.terminate())
- }
-
- def printRequestMethodAndResponseStatus(req: HttpRequest)(res: RouteResult): Unit = {
- println(req)
- println(res)
- }
-
- def botRoutes(hookId: String): Route = {
- path(hookId) {
- post {
- entity(as[Update]) { _ =>
- handleWith(receivedUpdate)
- }
- }
- }
- }
-
- private def receivedUpdate(update: Update): Future[HttpResponse] = {
- import akka.actor.typed.scaladsl.AskPattern._
-
- implicit val timeout: Timeout = 3.seconds
- implicit val scheduler: Scheduler = updateSystem.scheduler
-
- val result: Future[UpdateActor.UpdateResponse] = updateSystem ? (ref => UpdateActor.UpdateReceived(update, ref))
-
- result.andThen {
- case Success(response) => sendResponse(response.chatId, response.text)
- case Failure(ex) => println("Failed to process message...")
- }
-
- result.map(res => HttpResponse()).fallbackTo(Future.successful(HttpResponse()))
- }
-
- private def sendResponse(chatId: Long, text: String) = {
- import io.circe._, io.circe.generic.auto._, io.circe.syntax._
-
- val sendMessage = SendMessage(chatId, text)
- val printer = Printer.noSpaces.copy(dropNullValues = true)
- val json = printer.pretty(sendMessage.asJson)
- val request = HttpRequest(HttpMethods.POST, uri = botUri.sendMessage, entity = HttpEntity.Strict(ContentTypes.`application/json`, ByteString(json)))
- http.singleRequest(request)
- }
-
- def getBotInfo: Future[Response[GetMe]] = {
- http.singleRequest(HttpRequest(uri = botUri.getMe)).flatMap(Unmarshal(_).to[Response[GetMe]])
- }
-
- private def setWebhook(): Future[HttpResponse] = {
- print("Setting webhook...")
- val urlEntity = HttpEntity.Strict(ContentTypes.`text/plain(UTF-8)`, ByteString(webhookUri.toString()))
- val urlPart = Multipart.FormData.BodyPart.Strict("url", urlEntity)
-
- val certificate = ByteString(Source.fromResource("telegram-bot.pem").mkString)
- val certificateEntity = HttpEntity.Strict(ContentTypes.`application/octet-stream`, certificate)
- val certificatePart = Multipart.FormData.BodyPart.Strict("certificate", certificateEntity, Map("filename" -> "telegram-bot.pem"))
-
- val setWebhookFormData = Multipart.FormData.Strict(immutable.Seq(urlPart, certificatePart))
-
- Marshal(setWebhookFormData)
- .to[RequestEntity]
- .flatMap(requestEntity => http.singleRequest(HttpRequest(uri = botUri.setWebhook, method = HttpMethods.POST, entity = requestEntity)))
- .andThen {
- case Success(response) => println(s" ${response.status.value}")
- case Failure(exception) => println(s" failed with exception: ${exception.getMessage}")
- }
- }
-
- private def deleteWebhook(): Future[HttpResponse] = {
- print("Deleting webhook...")
- http
- .singleRequest(HttpRequest(uri = botUri.deleteWebhook, method = HttpMethods.POST))
- .andThen {
- case Success(response) => println(s" ${response.status.value}")
- case Failure(exception) => println(s" failed with exception: ${exception.getMessage}")
- }
- }
-
- def getWebhookInfo(): Future[Response[WebhookInfo]] = {
- http
- .singleRequest(HttpRequest(uri = botUri.getWebhookInfo, method = HttpMethods.GET))
- .flatMap(Unmarshal(_).to[Response[WebhookInfo]])
- }
-}
-
-object TelegramBotServer {
- private val botId = "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4"
-
- def apply(port: Int, httpsContext: Option[HttpsConnectionContext]): TelegramBotServer = new TelegramBotServer(botId, port, httpsContext)
-
- def main(args: Array[String]): Unit = {
- val httpsContext = createHttpsConnectionContext
- val tbs = TelegramBotServer(8443, Some(createHttpsConnectionContext))
-
- StdIn.readLine()
- tbs.stop()
- }
-
- def createHttpsConnectionContext: HttpsConnectionContext = {
- val password: Array[Char] = "".toCharArray // do not store passwords in code, read them from somewhere safe!
-
- val ks: KeyStore = KeyStore.getInstance("PKCS12")
- val keystore: InputStream = getClass.getResourceAsStream("/telegram-bot.p12")
-
- require(keystore != null, "Keystore required!")
- ks.load(keystore, password)
-
- val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
- keyManagerFactory.init(ks, password)
-
- val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
- tmf.init(ks)
-
- val sslContext: SSLContext = SSLContext.getInstance("TLS")
- sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
-
- ConnectionContext.https(sslContext)
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala b/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala
deleted file mode 100644
index 7ab6908..0000000
--- a/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala
+++ /dev/null
@@ -1,234 +0,0 @@
-package eu.xeppaka.bot1
-
-import com.vk.api.sdk.objects.base.Sticker
-import com.vk.api.sdk.objects.video.Video
-
-object TelegramEntities {
-
- case class Response[T](ok: Boolean,
- description: Option[String] = None,
- error_code: Option[Int] = None,
- result: T)
-
- case class GetMe(id: Int, is_bot: Boolean, first_name: String, username: String)
-
- case class InlineQuery(id: String,
- from: User,
- location: Location,
- query: String,
- offset: String)
-
- case class Location(longitude: Float,
- latitude: Float)
-
- case class Update(update_id: Int,
- message: Option[Message] = None,
- edited_message: Option[Message] = None,
- channel_post: Option[Message] = None,
- edited_channel_post: Option[Message] = None,
- inline_query: Option[InlineQuery] = None,
- chosen_inline_result: Option[ChosenInlineResult] = None,
- callback_query: Option[CallbackQuery] = None,
- shipping_query: Option[ShippingQuery] = None,
- pre_checkout_query: Option[PreCheckoutQuery] = None)
-
- case class ChosenInlineResult(result_id: String,
- from: User,
- location: Option[Location] = None,
- inline_message_id: Option[String] = None,
- query: String)
-
- case class CallbackQuery(id: String,
- from: User,
- message: Option[Message] = None,
- inline_message_id: Option[String] = None,
- chat_instance: String,
- data: Option[String] = None,
- game_short_name: Option[String] = None)
-
- case class ShippingQuery(id: String,
- from: User,
- invoice_payload: String,
- shipping_address: ShippingAddress)
-
- case class ShippingAddress(country_code: String,
- state: String,
- city: String,
- street_line1: String,
- street_line2: String,
- post_code: String)
-
- case class PreCheckoutQuery(id: String,
- from: User,
- currency: String,
- total_amount: Int,
- invoice_payload: String,
- shipping_option_id: Option[String] = None,
- order_info: Option[OrderInfo] = None)
-
- case class OrderInfo(name: Option[String] = None,
- phone_number: Option[String] = None,
- email: Option[String] = None,
- shipping_address: Option[ShippingAddress] = None)
-
- case class User(id: Int,
- is_bot: Boolean,
- first_name: String,
- last_name: Option[String] = None,
- username: Option[String] = None,
- language_code: Option[String] = None)
-
- case class SendMessage(chat_id: Long,
- text: String,
- parse_mode: Option[String] = None,
- disable_web_page_preview: Option[Boolean] = None,
- disable_notification: Option[Boolean] = None,
- reply_to_message_id: Option[Int] = None,
- reply_markup: Option[String] = None)
-
- case class Message(message_id: Int,
- from: Option[User] = None,
- date: Int,
- chat: Chat,
- forward_from: Option[User] = None,
- forward_from_chat: Option[User] = None,
- forward_from_message_id: Option[Int] = None,
- forward_signature: Option[String] = None,
- forward_date: Option[Int] = None,
- reply_to_message: Option[Message] = None,
- edit_date: Option[Int] = None,
- media_group_id: Option[String] = None,
- author_signature: Option[String] = None,
- text: Option[String] = None,
- entities: Option[Seq[MessageEntity]] = None,
- caption_entities: Option[Seq[MessageEntity]] = None,
- audio: Option[Audio] = None,
- document: Option[Document] = None,
- game: Option[Game] = None,
- photo: Option[Seq[PhotoSize]] = None,
- sticker: Option[Sticker] = None,
- video: Option[Video] = None,
- voice: Option[Voice] = None,
- video_note: Option[VideoNote] = None,
- caption: Option[String] = None,
- contact: Option[Contact] = None,
- location: Option[Location] = None,
- venue: Option[Venue] = None,
- new_chat_members: Option[Seq[User]] = None,
- left_chat_member: Option[Seq[User]] = None,
- new_chat_title: Option[String] = None,
- new_chat_photo: Option[Seq[PhotoSize]] = None,
- delete_chat_photo: Option[Boolean] = None,
- group_chat_created: Option[Boolean] = None,
- supergroup_chat_created: Option[Boolean] = None,
- channel_chat_created: Option[Boolean] = None,
- migrate_to_chat_id: Option[Int] = None,
- migrate_from_chat_id: Option[Int] = None,
- pinned_message: Option[Message] = None,
- invoice: Option[Invoice] = None,
- successful_payment: Option[SuccessfulPayment] = None,
- connected_website: Option[String] = None)
-
- case class MessageEntity(`type`: String,
- offset: Int,
- length: Int,
- url: Option[String] = None,
- user: Option[User] = None)
-
- case class Contact(phone_number: String,
- first_name: String,
- last_name: Option[String] = None,
- user_id: Option[Int] = None)
-
- case class Audio(file_id: String,
- duration: Int,
- performer: Option[String] = None,
- title: Option[String] = None,
- mime_type: Option[String] = None,
- file_size: Option[Int] = None)
-
- case class Document(file_id: String,
- thumb: Option[PhotoSize] = None,
- file_name: Option[String] = None,
- mime_type: Option[String] = None,
- file_size: Option[Int] = None)
-
- case class PhotoSize(file_id: String,
- width: Int,
- height: Int,
- file_size: Option[Int] = None)
-
- case class Voice(file_id: String,
- duration: Int,
- mime_type: Option[String] = None,
- file_size: Option[Int] = None)
-
- case class VideoNote(file_id: String,
- length: Int,
- duration: Int,
- thumb: Option[PhotoSize] = None,
- file_size: Option[Int] = None)
-
- case class ChatPhoto(small_file_id: String, big_file_id: String)
-
- case class Chat(id: Long,
- `type`: String,
- title: Option[String] = None,
- username: Option[String] = None,
- first_name: Option[String] = None,
- last_name: Option[String] = None,
- all_members_are_administrators: Option[Boolean] = None,
- photo: Option[ChatPhoto] = None,
- description: Option[String] = None,
- invite_link: Option[String] = None,
- pinned_message: Option[Message] = None,
- sticker_set_name: Option[String] = None,
- can_set_sticker_set: Option[Boolean] = None)
-
- case class Game(title: String,
- description: String,
- photo: Seq[PhotoSize],
- text: Option[String] = None,
- text_entities: Option[Seq[MessageEntity]] = None,
- animation: Option[Animation] = None)
-
- case class Animation(file_id: String,
- thumb: Option[PhotoSize] = None,
- file_name: Option[String] = None,
- mime_type: Option[String] = None,
- file_size: Option[Int] = None)
-
- case class InputFile()
-
- case class Venue(location: Location,
- title: String,
- address: String,
- foursquare_id: Option[String] = None)
-
- case class Invoice(title: String,
- description: String,
- start_parameter: String,
- currency: String,
- total_amount: Int)
-
- case class SuccessfulPayment(currency: String,
- total_amount: Int,
- invoice_payload: String,
- shipping_option_id: Option[String] = None,
- order_info: Option[OrderInfo] = None,
- telegram_payment_charge_id: String,
- provider_payment_charge_id: String)
-
- case class Webhook(url: String,
- certificate: Option[InputFile] = None,
- max_connections: Option[Int] = None,
- allowed_updates: Option[Seq[String]] = None)
-
- case class WebhookInfo(url: String,
- has_custom_certificate: Boolean,
- pending_update_count: Int,
- last_error_date: Option[Int] = None,
- last_error_message: Option[String] = None,
- max_connections: Option[Int] = None,
- allowed_updates: Option[Seq[String]] = None)
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/VkApiTests.scala b/src/main/scala/eu/xeppaka/bot1/VkApiTests.scala
deleted file mode 100644
index 70e6c3a..0000000
--- a/src/main/scala/eu/xeppaka/bot1/VkApiTests.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-package eu.xeppaka.bot1
-
-import akka.actor.ActorSystem
-import com.vk.api.sdk.client.VkApiClient
-import com.vk.api.sdk.client.actors.UserActor
-import com.vk.api.sdk.httpclient.HttpTransportClient
-
-object VkApiTests {
- private val APP_ID = 6472591
- private val APP_SECRET = "iGClqTncCZpzEqD8m5Wt"
- private val USER_SECRET_CODE = "4180052ce419f0470d"
-
- private val vkTransportClient = HttpTransportClient.getInstance()
- private val vkApiClient = new VkApiClient(vkTransportClient)
-
- // private val actorSystem = ActorSystem("vk")
-
- def main(args: Array[String]): Unit = {
- // val accessTokenActor = actorSystem.actorOf(AccessTokenActor.props)
- // accessTokenActor ! GetToken(APP_ID)
-
-// val authResponse = vkApiClient.oauth()
-// .userAuthorizationCodeFlow(APP_ID, APP_SECRET, "https://oauth.vk.com/blank.html", USER_SECRET_CODE)
-// .execute()
-
- val userActor = new UserActor(6242549, "e7acb4be42aa11ab692b567ae272f16ec3c49d20f51851bb507126ce9ffc0f4aea013d34c0a26e285202d")
-
- val resp = vkApiClient.messages().get(userActor).execute()
- println(resp)
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/actors/AccessTokenActor.scala b/src/main/scala/eu/xeppaka/bot1/actors/AccessTokenActor.scala
deleted file mode 100644
index 4f90355..0000000
--- a/src/main/scala/eu/xeppaka/bot1/actors/AccessTokenActor.scala
+++ /dev/null
@@ -1,40 +0,0 @@
-package eu.xeppaka.bot1.actors
-
-import akka.actor.{Actor, Props}
-import akka.http.scaladsl.Http
-import akka.http.scaladsl.model.{HttpEntity, HttpRequest, HttpResponse, StatusCodes}
-import akka.stream.{ActorMaterializer, ActorMaterializerSettings}
-import eu.xeppaka.bot1.actors.AccessTokenActor.GetToken
-
-import scala.concurrent.duration._
-
-class AccessTokenActor extends Actor {
- import akka.pattern.pipe
- import context.dispatcher
-
- private val OAUTH_URL_TEMPLATE = "https://oauth.vk.com/authorize?client_id=%d&scope=friends&redirect_uri=https://oauth.vk.com/blank.html&display=page&v=5.74&response_type=token"
- private implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(context.system))
- private val http = Http(context.system)
-
- override def receive: Receive = {
- case GetToken(clientId) =>
- val uri = OAUTH_URL_TEMPLATE.format(clientId)
- val request = HttpRequest(uri = uri)
-
- http.singleRequest(request).pipeTo(self)
- case HttpResponse(StatusCodes.OK, headers, entity, _) =>
- println("OK")
- entity.withoutSizeLimit().toStrict(1 second).pipeTo(self)
- case resp @ HttpResponse(code, _, _, _) =>
- println(s"Failed with HTTP code $code")
- case HttpEntity.Strict(contentType, data) =>
- println(data.utf8String)
- }
-}
-
-object AccessTokenActor {
-
- case class GetToken(clientId: Int)
-
- def props: Props = Props[AccessTokenActor]
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/actors/ChatActor.scala b/src/main/scala/eu/xeppaka/bot1/actors/ChatActor.scala
deleted file mode 100644
index f034925..0000000
--- a/src/main/scala/eu/xeppaka/bot1/actors/ChatActor.scala
+++ /dev/null
@@ -1,49 +0,0 @@
-package eu.xeppaka.bot1.actors
-
-import akka.actor.typed.scaladsl.Behaviors
-import akka.actor.typed.{ActorRef, Behavior}
-
-object ChatActor {
- sealed trait ChatCommand {
- def replyTo: ActorRef[Response]
- }
-
- case class Response(text: String)
- case class Help(replyTo: ActorRef[Response]) extends ChatCommand
- case class Start(replyTo: ActorRef[Response]) extends ChatCommand
- case class MessageReceived(text: String, replyTo: ActorRef[Response]) extends ChatCommand
- case class Stop(replyTo: ActorRef[Response]) extends ChatCommand
-
- private val started: Behavior[ChatCommand] = Behaviors.receive { (ctx, msg) =>
- msg match {
- case MessageReceived(text, replyTo) =>
- replyTo ! Response(s"Ok, you said: $text")
- Behaviors.same
- case Help(replyTo) =>
- replyHelp(replyTo)
- Behaviors.same
- case Stop(replyTo) =>
- replyTo ! Response("Bye, bye!")
- initial
- case _ =>
- Behaviors.unhandled
- }
- }
-
- private val initial: Behavior[ChatCommand] = Behaviors.receive { (ctx, msg) =>
- msg match {
- case Start(replyTo) =>
- replyTo ! Response("You started. Try /help, motherfucker...")
- started
- case c: ChatCommand =>
- c.replyTo ! Response("Only /start command is supported. Try it...")
- Behaviors.same
- }
- }
-
- private def replyHelp(replyTo: ActorRef[Response]): Unit = {
- replyTo ! Response("No help is provided for such motherfuckers like you! But... ok, send /stop and we are free.")
- }
-
- val behavior: Behavior[ChatCommand] = initial
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/actors/UpdateActor.scala b/src/main/scala/eu/xeppaka/bot1/actors/UpdateActor.scala
deleted file mode 100644
index 5b603c2..0000000
--- a/src/main/scala/eu/xeppaka/bot1/actors/UpdateActor.scala
+++ /dev/null
@@ -1,78 +0,0 @@
-package eu.xeppaka.bot1.actors
-
-import akka.NotUsed
-import akka.actor.typed.receptionist.Receptionist.Find
-import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
-import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
-import akka.actor.typed.{ActorRef, Behavior}
-import eu.xeppaka.bot1.TelegramEntities
-import eu.xeppaka.bot1.actors.ChatActor.{Help, MessageReceived, Start, Stop}
-
-object UpdateActor {
- object BotMessages {
- val start = "/start"
- val stop = "/stop"
- val help = "/help"
- }
-
- sealed trait UpdateCommand
- case class UpdateReceived(update: TelegramEntities.Update, replyTo: ActorRef[UpdateResponse]) extends UpdateCommand
- case class UpdateResponse(chatId: Long, text: String)
-
- val behavior: Behavior[UpdateCommand] = Behaviors.receive[UpdateCommand] { (ctx, msg) =>
- msg match {
- case UpdateReceived(receivedUpdate, replyTo) =>
- if (receivedUpdate.message.isDefined) {
- ctx.spawn(processMessage(ctx, receivedUpdate.message.get, replyTo), s"process-update-${receivedUpdate.update_id}")
- }
-
- Behaviors.same
- }
- }
-
- private def processMessage(parentContext: ActorContext[UpdateCommand], message: TelegramEntities.Message, replyTo: ActorRef[UpdateResponse]): Behavior[NotUsed] = {
- Behaviors.setup[AnyRef] { ctx =>
- val chatId = message.chat.id
- val chatKey = ServiceKey[ChatActor.ChatCommand](chatId.toString)
-
- println(s"Sending Find to receptionist to find actor with id: $chatId")
-
- ctx.system.receptionist ! Find(chatKey, ctx.self.narrow[Receptionist.Listing])
-
- Behaviors.receive[AnyRef] { (ctx, msg) =>
- msg match {
- case chatKey.Listing(listing) =>
- if (listing.isEmpty) {
- println(s"Actor with id: $chatId not found")
- } else {
- println(s"Actor with id: $chatId is found")
- }
-
- val chat = listing.headOption
- .getOrElse({
- val chatActor = parentContext.spawn(ChatActor.behavior, chatId.toString)
- ctx.system.receptionist ! Receptionist.Register(chatKey, chatActor)
- chatActor
- })
-
- chat ! getChatMessage(message, ctx.self.narrow[ChatActor.Response])
- Behaviors.same
- case ChatActor.Response(text) =>
- replyTo ! UpdateResponse(chatId, text)
- Behaviors.stopped
- }
- }
- }
- }.narrow[NotUsed]
-
- private def getChatMessage(message: TelegramEntities.Message, replyTo: ActorRef[ChatActor.Response]): ChatActor.ChatCommand = {
- import BotMessages._
-
- message.text.getOrElse(help) match {
- case `start` => Start(replyTo)
- case `stop` => Stop(replyTo)
- case `help` => Help(replyTo)
- case msgText@_ => MessageReceived(msgText, replyTo)
- }
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot1/BotUri.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala
similarity index 94%
rename from src/main/scala/eu/xeppaka/bot1/BotUri.scala
rename to telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala
index 0b704d5..82040c0 100644
--- a/src/main/scala/eu/xeppaka/bot1/BotUri.scala
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala
@@ -1,4 +1,4 @@
-package eu.xeppaka.bot1
+package eu.xeppaka.bot
import akka.http.scaladsl.model.Uri
@@ -6,14 +6,9 @@ case class BotUri(botId: String) {
private val baseUri = Uri(s"https://api.telegram.org/bot$botId")
val botUri: Uri = baseUri
-
val getMe: Uri = baseUri.withPath(baseUri.path / "getMe")
-
val setWebhook: Uri = baseUri.withPath(baseUri.path / "setWebhook")
-
val deleteWebhook: Uri = baseUri.withPath(baseUri.path / "deleteWebhook")
-
val getWebhookInfo: Uri = baseUri.withPath(baseUri.path / "getWebhookInfo")
-
val sendMessage: Uri = baseUri.withPath(baseUri.path / "sendMessage")
}
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala
new file mode 100644
index 0000000..814529a
--- /dev/null
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala
@@ -0,0 +1,30 @@
+package eu.xeppaka.bot
+
+import akka.actor.Scheduler
+import akka.actor.typed.scaladsl.AskPattern._
+import akka.actor.typed.{ActorSystem, DispatcherSelector}
+import eu.xeppaka.bot.TelegramBot._
+
+import scala.concurrent.duration._
+import scala.concurrent.{Await, ExecutionContextExecutor, Future}
+import scala.io.StdIn
+
+object Main {
+ def main(args: Array[String]): Unit = {
+ val botId = "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4"
+ val telegramBot = ActorSystem(TelegramBot.behavior(botId, "lenovo", 8443), "telegram-bot")
+ implicit val executionContext: ExecutionContextExecutor = telegramBot.dispatchers.lookup(DispatcherSelector.default())
+ implicit val scheduler: Scheduler = telegramBot.scheduler
+
+ val startFuture: Future[StartResult] = (telegramBot ? (ref => TelegramBot.Start(ref))) (10.seconds, scheduler)
+
+ println("Press enter to finish bot...")
+ StdIn.readLine()
+
+ val terminateFuture = startFuture
+ .flatMap { _ => (telegramBot ? (ref => TelegramBot.Stop(ref))) (10.seconds, scheduler) }
+ .andThen { case _ => telegramBot.terminate() }
+
+ Await.ready(terminateFuture, 5.seconds)
+ }
+}
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala
new file mode 100644
index 0000000..818cb46
--- /dev/null
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala
@@ -0,0 +1,276 @@
+package eu.xeppaka.bot
+
+import java.io.InputStream
+import java.security.{KeyStore, SecureRandom}
+import java.util.UUID
+
+import akka.Done
+import akka.actor.ActorSystem
+import akka.actor.typed.scaladsl.adapter._
+import akka.actor.typed.scaladsl.{Behaviors, StashBuffer}
+import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector}
+import akka.http.scaladsl.marshalling.Marshal
+import akka.http.scaladsl.model._
+import akka.http.scaladsl.server.Directives.{as, complete, entity, path, post}
+import akka.http.scaladsl.server.Route
+import akka.http.scaladsl.{ConnectionContext, Http, HttpExt, HttpsConnectionContext}
+import akka.stream.ActorMaterializer
+import akka.util.ByteString
+import eu.xeppaka.bot.TelegramEntities._
+import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
+
+import scala.collection.immutable
+import scala.concurrent.ExecutionContextExecutor
+import scala.io.Source
+import scala.util.{Failure, Success}
+
+object TelegramBot {
+ sealed trait Command
+ sealed trait CommandResult
+ sealed trait StartResult extends CommandResult
+ sealed trait StopResult extends CommandResult
+
+ case object StartSuccess extends StartResult
+ case class StartFailure(exception: Throwable) extends StartResult
+ case object StopSuccess extends StopResult
+ case class StopFailure(exception: Throwable) extends StopResult
+
+ case class Start(replyTo: ActorRef[StartResult]) extends Command
+ case class Stop(replyTo: ActorRef[StopResult]) extends Command
+ case object GetBotInfo
+ case object GetWebhookInfo
+
+ def behavior(botId: String, interface: String, port: Int): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ implicit val untypedSystem: ActorSystem = ctx.system.toUntyped
+ implicit val actorMaterializer: ActorMaterializer = ActorMaterializer()
+ implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+
+ val botUri = BotUri(botId)
+ val http: HttpExt = Http()
+ val hookId = UUID.randomUUID().toString
+ val webhookUri = Uri(s"https://xeppaka.eu:88/$hookId")
+ val httpsContext = createHttpsConnectionContext
+ val stashBuffer = StashBuffer[Command](10)
+ val updatesProcessor = ctx.spawn(UpdatesProcessor.behavior, "updatesProcessor")
+ val routes = botRoutes(hookId, updatesProcessor)
+
+ def reply(command: Command, exceptions: Seq[Throwable]): Unit = {
+ command match {
+ case Start(replyTo) =>
+ if (exceptions.isEmpty) {
+ ctx.log.info("action=start_bot result=success")
+ replyTo ! StartSuccess
+ } else {
+ ctx.log.error("action=start_bot result=failure", exceptions.head)
+ replyTo ! StartFailure(exceptions.head)
+ }
+ case Stop(replyTo) =>
+ if (exceptions.isEmpty) {
+ ctx.log.info("action=stop_bot result=success")
+ replyTo ! StopSuccess
+ } else {
+ ctx.log.error("action=stop_bot result=failure", exceptions.head)
+ replyTo ! StopFailure(exceptions.head)
+ }
+ case _ => throw new IllegalArgumentException(s"Unsupported command to reply: $command.")
+ }
+ }
+
+ def stopped(replyOnCommand: Option[Command] = None, exceptions: Seq[Throwable] = Seq.empty): Behavior[Command] = Behaviors.setup { ctx =>
+ replyOnCommand.foreach(reply(_, exceptions))
+
+ Behaviors.receiveMessage[Command] {
+ case startCommand@Start(_) =>
+ ctx.log.info("action=start_bot")
+ bindingServer(startCommand)
+ case stopCommand@Stop(_) =>
+ ctx.log.info("action=stop_bot")
+ reply(stopCommand, Seq.empty)
+ Behaviors.same
+ case _ =>
+ Behaviors.unhandled
+ }
+ }
+
+ def bindingServer(replyOnCommand: Command): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ case class BindingSuccess(binding: Http.ServerBinding) extends Command
+ case class BindingFailure(exception: Throwable) extends Command
+
+ ctx.log.info("action=bind_server interface={} port={}", interface, port)
+
+ http
+ .bindAndHandle(routes, interface, port, httpsContext)
+ .onComplete {
+ case Success(binding) => ctx.self ! BindingSuccess(binding)
+ case Failure(exception) => ctx.self ! BindingFailure(exception)
+ }
+
+ Behaviors.receiveMessage[Command] {
+ case BindingSuccess(binding) =>
+ ctx.log.info("action=bind_server result=success")
+ stashBuffer.unstashAll(ctx, settingWebhook(binding, replyOnCommand, Seq.empty))
+ case BindingFailure(exception) =>
+ ctx.log.error("action=bind_server result=failure", exception)
+ stashBuffer.unstashAll(ctx, stopped(Some(replyOnCommand), Seq(exception)))
+ case otherCommand: Command =>
+ stashBuffer.stash(otherCommand)
+ Behaviors.same
+ }
+ }
+
+ def unbindingServer(binding: Http.ServerBinding, replyOnCommand: Command, exceptions: Seq[Throwable]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ case object UnbindingSuccess extends Command
+ case class UnbindingFailure(exception: Throwable) extends Command
+
+ ctx.log.info("action=unbind_server interface={} port={}", interface, port)
+
+ binding
+ .unbind()
+ .onComplete {
+ case Success(Done) => ctx.self ! UnbindingSuccess
+ case Failure(exception) => ctx.self ! UnbindingFailure(exception)
+ }
+
+ Behaviors.receiveMessage[Command] {
+ case UnbindingSuccess =>
+ ctx.log.info("action=unbind_server result=success")
+ stashBuffer.unstashAll(ctx, stopped(Some(replyOnCommand), exceptions))
+ case UnbindingFailure(exception) =>
+ ctx.log.error("action=unbind_server result=failure", exception)
+ stashBuffer.unstashAll(ctx, stopped(Some(replyOnCommand), exceptions :+ exception))
+ case otherCommand: Command =>
+ stashBuffer.stash(otherCommand)
+ Behaviors.same
+ }
+ }
+
+ def settingWebhook(binding: Http.ServerBinding, replyOnCommand: Command, exceptions: Seq[Throwable]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ case object SetWebhookSuccess extends Command
+ case class SetWebhookFailure(exception: Throwable) extends Command
+
+ ctx.log.info("action=set_webhook url={} webhook={}", botUri.setWebhook, webhookUri)
+
+ implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+
+ val urlEntity = HttpEntity.Strict(ContentTypes.`text/plain(UTF-8)`, ByteString(webhookUri.toString()))
+ val urlPart = Multipart.FormData.BodyPart.Strict("url", urlEntity)
+
+ val certificate = ByteString(Source.fromResource("telegram-bot.pem").mkString)
+ val certificateEntity = HttpEntity.Strict(ContentTypes.`application/octet-stream`, certificate)
+ val certificatePart = Multipart.FormData.BodyPart.Strict("certificate", certificateEntity, Map("filename" -> "telegram-bot.pem"))
+
+ val setWebhookFormData = Multipart.FormData.Strict(immutable.Seq(urlPart, certificatePart))
+
+ Marshal(setWebhookFormData)
+ .to[RequestEntity]
+ .flatMap(requestEntity => http.singleRequest(HttpRequest(uri = botUri.setWebhook, method = HttpMethods.POST, entity = requestEntity)))
+ .onComplete {
+ case Success(response) =>
+ if (response.status.isSuccess())
+ ctx.self ! SetWebhookSuccess
+ else
+ ctx.self ! SetWebhookFailure(new RuntimeException(s"Set webhook HTTP response status: ${response.status.value}."))
+ case Failure(exception) =>
+ ctx.self ! SetWebhookFailure(exception)
+ }
+
+ Behaviors.receiveMessage {
+ case SetWebhookSuccess =>
+ ctx.log.info("action=set_webhook result=success")
+ stashBuffer.unstashAll(ctx, started(binding, replyOnCommand, exceptions))
+ case SetWebhookFailure(exception) =>
+ ctx.log.error("action=set_webhook result=failure", exception)
+ stashBuffer.unstashAll(ctx, unbindingServer(binding, replyOnCommand, exceptions :+ exception))
+ case otherCommand: Command =>
+ stashBuffer.stash(otherCommand)
+ Behaviors.same
+ }
+ }
+
+ def deletingWebhook(binding: Http.ServerBinding, replyOnCommand: Command, exceptions: Seq[Throwable]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ case object DeleteWebhookSuccess extends Command
+ case class DeleteWebhookFailure(exception: Throwable) extends Command
+
+ ctx.log.info("action=delete_webhook url={} webhook={}", botUri.deleteWebhook, webhookUri)
+
+ implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+
+ http
+ .singleRequest(HttpRequest(uri = botUri.deleteWebhook, method = HttpMethods.POST))
+ .onComplete {
+ case Success(response) =>
+ if (response.status.isSuccess())
+ ctx.self ! DeleteWebhookSuccess
+ else
+ ctx.self ! DeleteWebhookFailure(new RuntimeException(s"Delete webhook HTTP response status: ${response.status.value}"))
+ case Failure(exception) =>
+ ctx.self ! DeleteWebhookFailure(exception)
+ }
+
+ Behaviors.receiveMessage {
+ case DeleteWebhookSuccess =>
+ ctx.log.info("action=delete_webhook result=success")
+ stashBuffer.unstashAll(ctx, unbindingServer(binding, replyOnCommand, exceptions))
+ case DeleteWebhookFailure(exception) =>
+ ctx.log.error("action=delete_webhook result=failure", exception)
+ stashBuffer.unstashAll(ctx, unbindingServer(binding, replyOnCommand, exceptions :+ exception))
+ case otherCommand: Command =>
+ stashBuffer.stash(otherCommand)
+ Behaviors.same
+ }
+ }
+
+ def started(binding: Http.ServerBinding, replyOnCommand: Command, exceptions: Seq[Throwable]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ reply(replyOnCommand, exceptions)
+
+ Behaviors.receiveMessage[Command] {
+ case startCommand@Start(_) =>
+ ctx.log.info("action=start_bot")
+ reply(startCommand, Seq.empty)
+ Behaviors.same
+ case stopCommand@Stop(_) =>
+ ctx.log.info("action=stop_bot")
+ deletingWebhook(binding, stopCommand, exceptions)
+ case _ =>
+ Behaviors.unhandled
+ }
+ }
+
+ stopped()
+ }
+
+ private def botRoutes(hookId: String, updatesProcessor: ActorRef[UpdatesProcessor.Command]): Route = {
+ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
+ import io.circe.generic.auto._
+
+ path(hookId) {
+ post {
+ entity(as[Update]) { update =>
+ updatesProcessor ! UpdatesProcessor.UpdateReceived(update)
+ complete(HttpResponse())
+ }
+ }
+ }
+ }
+
+ private def createHttpsConnectionContext: HttpsConnectionContext = {
+ val password: Array[Char] = "".toCharArray // do not store passwords in code, read them from somewhere safe!
+
+ val ks: KeyStore = KeyStore.getInstance("PKCS12")
+ val keystore: InputStream = getClass.getResourceAsStream("/telegram-bot.p12")
+
+ require(keystore != null, "Keystore required!")
+ ks.load(keystore, password)
+
+ val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
+ keyManagerFactory.init(ks, password)
+
+ val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
+ tmf.init(ks)
+
+ val sslContext: SSLContext = SSLContext.getInstance("TLS")
+ sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
+
+ ConnectionContext.https(sslContext)
+ }
+}
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala
new file mode 100644
index 0000000..ab85743
--- /dev/null
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala
@@ -0,0 +1,278 @@
+package eu.xeppaka.bot
+
+object TelegramEntities {
+
+ case class Response[T](ok: Boolean,
+ description: Option[String] = None,
+ error_code: Option[Int] = None,
+ result: T
+ )
+
+ case class GetMe(id: Int, is_bot: Boolean, first_name: String, username: String)
+
+ case class InlineQuery(id: String,
+ from: User,
+ location: Location,
+ query: String,
+ offset: String
+ )
+
+ case class Location(longitude: Float,
+ latitude: Float
+ )
+
+ case class Update(update_id: Int,
+ message: Option[Message] = None,
+ edited_message: Option[Message] = None,
+ channel_post: Option[Message] = None,
+ edited_channel_post: Option[Message] = None,
+ inline_query: Option[InlineQuery] = None,
+ chosen_inline_result: Option[ChosenInlineResult] = None,
+ callback_query: Option[CallbackQuery] = None,
+ shipping_query: Option[ShippingQuery] = None,
+ pre_checkout_query: Option[PreCheckoutQuery] = None
+ )
+
+ case class ChosenInlineResult(result_id: String,
+ from: User,
+ location: Option[Location] = None,
+ inline_message_id: Option[String] = None,
+ query: String
+ )
+
+ case class CallbackQuery(id: String,
+ from: User,
+ message: Option[Message] = None,
+ inline_message_id: Option[String] = None,
+ chat_instance: String,
+ data: Option[String] = None,
+ game_short_name: Option[String] = None
+ )
+
+ case class ShippingQuery(id: String,
+ from: User,
+ invoice_payload: String,
+ shipping_address: ShippingAddress
+ )
+
+ case class ShippingAddress(country_code: String,
+ state: String,
+ city: String,
+ street_line1: String,
+ street_line2: String,
+ post_code: String
+ )
+
+ case class PreCheckoutQuery(id: String,
+ from: User,
+ currency: String,
+ total_amount: Int,
+ invoice_payload: String,
+ shipping_option_id: Option[String] = None,
+ order_info: Option[OrderInfo] = None
+ )
+
+ case class OrderInfo(name: Option[String] = None,
+ phone_number: Option[String] = None,
+ email: Option[String] = None,
+ shipping_address: Option[ShippingAddress] = None
+ )
+
+ case class User(id: Int,
+ is_bot: Boolean,
+ first_name: String,
+ last_name: Option[String] = None,
+ username: Option[String] = None,
+ language_code: Option[String] = None
+ )
+
+ case class SendMessage(chat_id: Long,
+ text: String,
+ parse_mode: Option[String] = None,
+ disable_web_page_preview: Option[Boolean] = None,
+ disable_notification: Option[Boolean] = None,
+ reply_to_message_id: Option[Int] = None,
+ reply_markup: Option[String] = None
+ )
+
+ case class Message(message_id: Int,
+ from: Option[User] = None,
+ date: Int,
+ chat: Chat,
+ forward_from: Option[User] = None,
+ forward_from_chat: Option[User] = None,
+ forward_from_message_id: Option[Int] = None,
+ forward_signature: Option[String] = None,
+ forward_date: Option[Int] = None,
+ reply_to_message: Option[Message] = None,
+ edit_date: Option[Int] = None,
+ media_group_id: Option[String] = None,
+ author_signature: Option[String] = None,
+ text: Option[String] = None,
+ entities: Option[Seq[MessageEntity]] = None,
+ caption_entities: Option[Seq[MessageEntity]] = None,
+ audio: Option[Audio] = None,
+ document: Option[Document] = None,
+ game: Option[Game] = None,
+ photo: Option[Seq[PhotoSize]] = None,
+ sticker: Option[Sticker] = None,
+ video: Option[Video] = None,
+ voice: Option[Voice] = None,
+ video_note: Option[VideoNote] = None,
+ caption: Option[String] = None,
+ contact: Option[Contact] = None,
+ location: Option[Location] = None,
+ venue: Option[Venue] = None,
+ new_chat_members: Option[Seq[User]] = None,
+ left_chat_member: Option[Seq[User]] = None,
+ new_chat_title: Option[String] = None,
+ new_chat_photo: Option[Seq[PhotoSize]] = None,
+ delete_chat_photo: Option[Boolean] = None,
+ group_chat_created: Option[Boolean] = None,
+ supergroup_chat_created: Option[Boolean] = None,
+ channel_chat_created: Option[Boolean] = None,
+ migrate_to_chat_id: Option[Int] = None,
+ migrate_from_chat_id: Option[Int] = None,
+ pinned_message: Option[Message] = None,
+ invoice: Option[Invoice] = None,
+ successful_payment: Option[SuccessfulPayment] = None,
+ connected_website: Option[String] = None
+ )
+
+ case class MessageEntity(`type`: String,
+ offset: Int,
+ length: Int,
+ url: Option[String] = None,
+ user: Option[User] = None
+ )
+
+ case class Contact(phone_number: String,
+ first_name: String,
+ last_name: Option[String] = None,
+ user_id: Option[Int] = None
+ )
+
+ case class Sticker(file_id: String,
+ width: Int,
+ height: Int,
+ thumb: Option[PhotoSize] = None,
+ emoji: Option[String] = None,
+ set_name: Option[String] = None,
+ mask_position: Option[String] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class Video(file_id: String,
+ width: Int,
+ height: Int,
+ duration: Int,
+ thumb: Option[PhotoSize] = None,
+ mime_type: Option[String] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class Audio(file_id: String,
+ duration: Int,
+ performer: Option[String] = None,
+ title: Option[String] = None,
+ mime_type: Option[String] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class Document(file_id: String,
+ thumb: Option[PhotoSize] = None,
+ file_name: Option[String] = None,
+ mime_type: Option[String] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class PhotoSize(file_id: String,
+ width: Int,
+ height: Int,
+ file_size: Option[Int] = None
+ )
+
+ case class Voice(file_id: String,
+ duration: Int,
+ mime_type: Option[String] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class VideoNote(file_id: String,
+ length: Int,
+ duration: Int,
+ thumb: Option[PhotoSize] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class ChatPhoto(small_file_id: String, big_file_id: String)
+
+ case class Chat(id: Long,
+ `type`: String,
+ title: Option[String] = None,
+ username: Option[String] = None,
+ first_name: Option[String] = None,
+ last_name: Option[String] = None,
+ all_members_are_administrators: Option[Boolean] = None,
+ photo: Option[ChatPhoto] = None,
+ description: Option[String] = None,
+ invite_link: Option[String] = None,
+ pinned_message: Option[Message] = None,
+ sticker_set_name: Option[String] = None,
+ can_set_sticker_set: Option[Boolean] = None
+ )
+
+ case class Game(title: String,
+ description: String,
+ photo: Seq[PhotoSize],
+ text: Option[String] = None,
+ text_entities: Option[Seq[MessageEntity]] = None,
+ animation: Option[Animation] = None
+ )
+
+ case class Animation(file_id: String,
+ thumb: Option[PhotoSize] = None,
+ file_name: Option[String] = None,
+ mime_type: Option[String] = None,
+ file_size: Option[Int] = None
+ )
+
+ case class InputFile()
+
+ case class Venue(location: Location,
+ title: String,
+ address: String,
+ foursquare_id: Option[String] = None
+ )
+
+ case class Invoice(title: String,
+ description: String,
+ start_parameter: String,
+ currency: String,
+ total_amount: Int
+ )
+
+ case class SuccessfulPayment(currency: String,
+ total_amount: Int,
+ invoice_payload: String,
+ shipping_option_id: Option[String] = None,
+ order_info: Option[OrderInfo] = None,
+ telegram_payment_charge_id: String,
+ provider_payment_charge_id: String
+ )
+
+ case class Webhook(url: String,
+ certificate: Option[InputFile] = None,
+ max_connections: Option[Int] = None,
+ allowed_updates: Option[Seq[String]] = None
+ )
+
+ case class WebhookInfo(url: String,
+ has_custom_certificate: Boolean,
+ pending_update_count: Int,
+ last_error_date: Option[Int] = None,
+ last_error_message: Option[String] = None,
+ max_connections: Option[Int] = None,
+ allowed_updates: Option[Seq[String]] = None
+ )
+}
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/UpdatesProcessor.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/UpdatesProcessor.scala
new file mode 100644
index 0000000..8a52bef
--- /dev/null
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/UpdatesProcessor.scala
@@ -0,0 +1,20 @@
+package eu.xeppaka.bot
+
+import akka.actor.typed.Behavior
+import akka.actor.typed.scaladsl.Behaviors
+import eu.xeppaka.bot.TelegramEntities.Update
+
+object UpdatesProcessor {
+ sealed trait Command
+ sealed trait CommandResult
+
+ case class UpdateReceived(update: Update) extends Command
+
+ def behavior: Behavior[Command] = Behaviors.receive[Command] { (ctx, msg) =>
+ msg match {
+ case UpdateReceived(update) =>
+ ctx.log.info("action=update_received update={}", update)
+ Behaviors.same
+ }
+ }
+}