diff --git a/.idea/modules/root-build.iml b/.idea/modules/root-build.iml index ee53ba2..f493bcb 100644 --- a/.idea/modules/root-build.iml +++ b/.idea/modules/root-build.iml @@ -1,5 +1,5 @@ - + @@ -14,6 +14,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/modules/root.iml b/.idea/modules/root.iml index b3e3660..435b95b 100644 --- a/.idea/modules/root.iml +++ b/.idea/modules/root.iml @@ -15,8 +15,6 @@ - - @@ -32,7 +30,6 @@ - @@ -65,5 +62,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.sbt b/build.sbt index d2eba4d..b601b8f 100644 --- a/build.sbt +++ b/build.sbt @@ -5,15 +5,25 @@ lazy val root = (project in file(".")). inThisBuild(List( organization := "com.example", scalaVersion := "2.12.6", - version := "0.1.0-SNAPSHOT" + version := "0.1.0-SNAPSHOT", + mainClass := Some("eu.xeppaka.bot1.TelegramBotServer") )), name := "telegram-bot1", libraryDependencies ++= Seq( scalaTest % Test, akka, akkaHttp, - akkaHttpSprayJson, akkaStream, - vkapi + vkapi, + circleCore, + circleGeneric, + circleParser ) ) + +assemblyMergeStrategy in assembly := { + case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first + case x => + val oldStrategy = (assemblyMergeStrategy in assembly).value + oldStrategy(x) +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d61a7ea..be9ca0f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,7 +4,9 @@ object Dependencies { lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" lazy val akka = "com.typesafe.akka" %% "akka-actor" % "2.5.12" lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.1" - lazy val akkaHttpSprayJson = "com.typesafe.akka" %% "akka-http-spray-json" % "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/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..652a3b9 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 253d94d..8ea5900 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,3 +1,3 @@ akka { - loglevel = "DEBUG" + loglevel = "INFO" } \ No newline at end of file diff --git a/src/main/resources/telegram-bot.p12 b/src/main/resources/telegram-bot.p12 new file mode 100644 index 0000000..8147227 Binary files /dev/null and b/src/main/resources/telegram-bot.p12 differ diff --git a/src/main/resources/telegram-bot.pem b/src/main/resources/telegram-bot.pem new file mode 100644 index 0000000..0fe289b --- /dev/null +++ b/src/main/resources/telegram-bot.pem @@ -0,0 +1,21 @@ +-----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/BotUri.scala b/src/main/scala/eu/xeppaka/bot1/BotUri.scala index d33ad97..0b704d5 100644 --- a/src/main/scala/eu/xeppaka/bot1/BotUri.scala +++ b/src/main/scala/eu/xeppaka/bot1/BotUri.scala @@ -5,6 +5,8 @@ import akka.http.scaladsl.model.Uri 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") @@ -12,4 +14,6 @@ case class BotUri(botId: String) { 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/src/main/scala/eu/xeppaka/bot1/CircleSupport.scala b/src/main/scala/eu/xeppaka/bot1/CircleSupport.scala new file mode 100644 index 0000000..35237b2 --- /dev/null +++ b/src/main/scala/eu/xeppaka/bot1/CircleSupport.scala @@ -0,0 +1,146 @@ +/* + * 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 index 061cd45..21918a4 100644 --- a/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala +++ b/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala @@ -4,24 +4,27 @@ import java.io.InputStream import java.security.{KeyStore, SecureRandom} import java.util.UUID -import akka.actor.ActorSystem +import akka.actor.{ActorSystem, Props} import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.directives.LoggingMagnet 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 com.typesafe.sslconfig.akka.AkkaSSLConfig +import akka.util.ByteString +import eu.xeppaka.bot1.actors.UpdateActor +import eu.xeppaka.bot1.actors.UpdateActor.ReceivedUpdate import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} +import scala.collection.immutable +import scala.concurrent.duration._ import scala.concurrent.{ExecutionContextExecutor, Future} -import scala.io.StdIn -import scala.util.{Failure, Success} +import scala.io.{Source, StdIn} class TelegramBotServer(botId: String, port: Int, httpsContext: Option[HttpsConnectionContext])(implicit val actorSystem: ActorSystem) { - + import FailFastCirceSupport._ + import io.circe.generic.auto._ import eu.xeppaka.bot1.TelegramEntities._ private val botUri = BotUri(botId) @@ -30,11 +33,12 @@ class TelegramBotServer(botId: String, port: Int, httpsContext: Option[HttpsConn private val http: HttpExt = Http() private val hookId = UUID.randomUUID().toString - private val webhookUri = Uri(s"https://xeppaka.eu:8443/$hookId") + private val webhookUri = Uri(s"https://xeppaka.eu:$port/$hookId") private val bindingFuture = http.bindAndHandle(botRoutes(hookId), - "127.0.0.1", + "pkcloud", port, connectionContext = httpsContext.getOrElse(http.defaultClientHttpsContext)) + private val updateActor = actorSystem.actorOf(UpdateActor.props(botUri, http)) println(s"webhook path: $webhookUri") @@ -53,25 +57,35 @@ class TelegramBotServer(botId: String, port: Int, httpsContext: Option[HttpsConn def botRoutes(hookId: String): Route = { path(hookId) { post { - logRequestResult(LoggingMagnet(_ => printRequestMethodAndResponseStatus)) { - onComplete(getBotInfo) { - case Success(res) => complete(res.ok.toString) - case Failure(ex) => complete(StatusCodes.InternalServerError, "Boooom!") - } + entity(as[Update]) { update => + handleWith(processUpdate) } } } } + def processUpdate(update: Update): HttpResponse = { + updateActor ! ReceivedUpdate(update) + HttpResponse() + } + def getBotInfo: Future[Response[GetMe]] = { http.singleRequest(HttpRequest(uri = botUri.getMe)).flatMap(Unmarshal(_).to[Response[GetMe]]) } def setWebhook(): Future[HttpResponse] = { - val hook = Webhook(webhookUri.toString()) - Marshal(hook) - .to[MessageEntity] - .flatMap(entity => http.singleRequest(HttpRequest(uri = botUri.setWebhook, method = HttpMethods.POST, entity = entity))) + 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))) // .flatMap(Unmarshal(_).to[Response[String]]) } @@ -97,14 +111,21 @@ object TelegramBotServer { val httpsContext = createHttpsConnectionContext implicit val actorSystem: ActorSystem = ActorSystem("telegram-bot") + implicit val materializer: ActorMaterializer = ActorMaterializer() implicit val executionContext: ExecutionContextExecutor = actorSystem.dispatcher - val tbs = TelegramBotServer(8443, Some(createHttpsConnectionContext)) - //tbs.setWebhook() + val tbs = TelegramBotServer(88, Some(createHttpsConnectionContext)) - tbs - .getWebhookInfo() - .onComplete(println(_)) + tbs.setWebhook() + .flatMap(response => response.entity.toStrict(5 seconds)) + .onComplete(entity => { + println(entity.get.data.utf8String) + entity.get.discardBytes() + }) + + // tbs + // .getWebhookInfo() + // .onComplete(println(_)) StdIn.readLine() @@ -113,10 +134,10 @@ object TelegramBotServer { } def createHttpsConnectionContext: HttpsConnectionContext = { - val password: Array[Char] = "changeit".toCharArray // do not store passwords in code, read them from somewhere safe! + 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.getClassLoader.getResourceAsStream("server.p12") + val keystore: InputStream = getClass.getResourceAsStream("/telegram-bot.p12") require(keystore != null, "Keystore required!") ks.load(keystore, password) diff --git a/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala b/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala index aaae2bc..e081e94 100644 --- a/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala +++ b/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala @@ -1,27 +1,17 @@ package eu.xeppaka.bot1 -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import spray.json._ +import com.vk.api.sdk.objects.base.Sticker +import com.vk.api.sdk.objects.video.Video -object TelegramEntities extends SprayJsonSupport with DefaultJsonProtocol { +object TelegramEntities { case class Response[T](ok: Boolean, description: Option[String] = None, error_code: Option[Int] = None, result: T) - object Response { - implicit val responseGetMeFormat: RootJsonFormat[Response[GetMe]] = jsonFormat4(Response[GetMe]) - implicit val responseWebhookInfoFormat: RootJsonFormat[Response[WebhookInfo]] = jsonFormat4(Response[WebhookInfo]) - implicit val responseStringFormat: RootJsonFormat[Response[String]] = jsonFormat4(Response[String]) - } - case class GetMe(id: Int, is_bot: Boolean, first_name: String, username: String) - object GetMe { - implicit val getMeFormat: RootJsonFormat[GetMe] = jsonFormat4(GetMe.apply) - } - case class InlineQuery(id: String, from: User, location: Location, @@ -88,7 +78,96 @@ object TelegramEntities extends SprayJsonSupport with DefaultJsonProtocol { username: Option[String] = None, language_code: Option[String] = None) - case class Message() + case class SendMessage(chat_id: Int, + 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) @@ -106,21 +185,45 @@ object TelegramEntities extends SprayJsonSupport with DefaultJsonProtocol { 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() - object InputFile { - implicit val inputFileFormat: RootJsonFormat[InputFile] = jsonFormat0(InputFile.apply) - } + 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) - object Webhook { - implicit val webHookFormat: RootJsonFormat[Webhook] = jsonFormat4(Webhook.apply) - } - case class WebhookInfo(url: String, has_custom_certificate: Boolean, pending_update_count: Int, @@ -128,8 +231,4 @@ object TelegramEntities extends SprayJsonSupport with DefaultJsonProtocol { last_error_message: Option[String] = None, max_connections: Option[Int] = None, allowed_updates: Option[Seq[String]] = None) - - object WebhookInfo { - implicit val webHookInfoFormat: RootJsonFormat[WebhookInfo] = jsonFormat7(WebhookInfo.apply) - } } diff --git a/src/main/scala/eu/xeppaka/bot1/AccessTokenActor.scala b/src/main/scala/eu/xeppaka/bot1/actors/AccessTokenActor.scala similarity index 93% rename from src/main/scala/eu/xeppaka/bot1/AccessTokenActor.scala rename to src/main/scala/eu/xeppaka/bot1/actors/AccessTokenActor.scala index 9881d2c..4f90355 100644 --- a/src/main/scala/eu/xeppaka/bot1/AccessTokenActor.scala +++ b/src/main/scala/eu/xeppaka/bot1/actors/AccessTokenActor.scala @@ -1,10 +1,10 @@ -package eu.xeppaka.bot1 +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.AccessTokenActor.GetToken +import eu.xeppaka.bot1.actors.AccessTokenActor.GetToken import scala.concurrent.duration._ diff --git a/src/main/scala/eu/xeppaka/bot1/DialogActor.scala b/src/main/scala/eu/xeppaka/bot1/actors/DialogActor.scala similarity index 89% rename from src/main/scala/eu/xeppaka/bot1/DialogActor.scala rename to src/main/scala/eu/xeppaka/bot1/actors/DialogActor.scala index c1556c6..3142b1b 100644 --- a/src/main/scala/eu/xeppaka/bot1/DialogActor.scala +++ b/src/main/scala/eu/xeppaka/bot1/actors/DialogActor.scala @@ -1,4 +1,4 @@ -package eu.xeppaka.bot1 +package eu.xeppaka.bot1.actors import java.util.UUID diff --git a/src/main/scala/eu/xeppaka/bot1/actors/UpdateActor.scala b/src/main/scala/eu/xeppaka/bot1/actors/UpdateActor.scala new file mode 100644 index 0000000..dd9ff07 --- /dev/null +++ b/src/main/scala/eu/xeppaka/bot1/actors/UpdateActor.scala @@ -0,0 +1,42 @@ +package eu.xeppaka.bot1.actors + +import akka.actor.{Actor, ActorLogging, Props} +import akka.http.scaladsl.HttpExt +import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpMethods, HttpRequest} +import akka.util.ByteString +import eu.xeppaka.bot1.TelegramEntities.SendMessage +import eu.xeppaka.bot1.{BotUri, TelegramEntities} + +class UpdateActor(botUri: BotUri, http: HttpExt) extends Actor with ActorLogging { + import UpdateActor.ReceivedUpdate + + override def receive: Receive = { + case ReceivedUpdate(update) => processUpdate(update) + } + + private def processUpdate(update: TelegramEntities.Update) = { + log.info(s"Received update: $update") + if (update.message.isDefined) { + processMessage(update.message.get) + } + } + + private def processMessage(message: TelegramEntities.Message) = { + import io.circe._, io.circe.generic.auto._, io.circe.syntax._ + + log.info("Received message from: {}", message.from) + val sendMessage = SendMessage(message.chat.id, s"Привет, ${message.from.get.first_name}") + 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) + } +} + +object UpdateActor { + + case class ReceivedUpdate(update: TelegramEntities.Update) + + def props(botUri: BotUri, http: HttpExt): Props = Props(new UpdateActor(botUri, http)) + +} diff --git a/src/test/scala/example/MarshalSpec.scala b/src/test/scala/example/MarshalSpec.scala index d7bfe69..0c62e8c 100644 --- a/src/test/scala/example/MarshalSpec.scala +++ b/src/test/scala/example/MarshalSpec.scala @@ -1,33 +1,37 @@ package example -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.Materializer import akka.util.ByteString +import eu.xeppaka.bot1.TelegramEntities +import eu.xeppaka.bot1.TelegramEntities.{Chat, Message, User} import org.scalatest.FlatSpec import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -import spray.json._ case class Text(t: Option[String], t1: Option[String]) -trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { - implicit val textFormat = jsonFormat2(Text) -} +class MarshalSpec extends FlatSpec { -class MarshalSpec extends FlatSpec with JsonSupport { + "Circle marshal/unmarshal" should "work" in { + import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._ + case class Large2(p1: Int, p2: Int, p3: Int, p4: Int, p5: Int, p6: Int, p7: Int, p8: Int, p9: Int, p10: Int, p11: Int, p12: Int, p13: Int, p14: Int, p15: Int, p16: Int, p17: Int, p18: Int, p19: Int, p20: Int, p21: Int, p22: Int, p23: Int) - "Marshal to MessageEntity" should "work" in { - implicit val mat: Materializer = null - val body = - """ - |{"t1": "some text value"} - """.stripMargin -// val bytes = ByteString(body) - val f = Unmarshal(body).to[Text] - val entity = Await.result(f, 1 second) - println(entity) + val f = Large2(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23) + + val json = f.asJson.noSpaces + println(json) + } + + "Telegram message marshal/unmarshal" should "work" in { + import io.circe._, io.circe.generic.auto._, io.circe.syntax._ + + implicit val printer: Printer = Printer.noSpaces.copy(dropNullValues = true) + val c = Chat(555666, "userChat") + val m = Message(message_id = 111222, from = None, date = 111333, chat = c) + val json = printer.pretty(m.asJson) + println(json) } }