New JSON parsing library - circle. Integrated with akka-http. Test sending message by bot.

This commit is contained in:
Pavel Kachalouski
2018-05-13 23:28:21 +02:00
parent 715514f9ec
commit 9690ef0a3d
16 changed files with 457 additions and 77 deletions

View File

@@ -1,3 +1,3 @@
akka {
loglevel = "DEBUG"
loglevel = "INFO"
}

Binary file not shown.

View File

@@ -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-----

View File

@@ -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")
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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._

View File

@@ -1,4 +1,4 @@
package eu.xeppaka.bot1
package eu.xeppaka.bot1.actors
import java.util.UUID

View File

@@ -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))
}