diff --git a/.idea/modules/root-build.iml b/.idea/modules/root-build.iml index ee53ba2..414f9c7 100644 --- a/.idea/modules/root-build.iml +++ b/.idea/modules/root-build.iml @@ -1,5 +1,5 @@ - + diff --git a/.idea/modules/root.iml b/.idea/modules/root.iml index 46ec688..7739fce 100644 --- a/.idea/modules/root.iml +++ b/.idea/modules/root.iml @@ -9,13 +9,51 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25,44 +63,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..253d94d --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,3 @@ +akka { + loglevel = "DEBUG" +} \ No newline at end of file diff --git a/src/main/scala/eu/xeppaka/bot1/BotUri.scala b/src/main/scala/eu/xeppaka/bot1/BotUri.scala new file mode 100644 index 0000000..d33ad97 --- /dev/null +++ b/src/main/scala/eu/xeppaka/bot1/BotUri.scala @@ -0,0 +1,15 @@ +package eu.xeppaka.bot1 + +import akka.http.scaladsl.model.Uri + +case class BotUri(botId: String) { + private val baseUri = Uri(s"https://api.telegram.org/bot$botId") + + 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") +} diff --git a/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala b/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala index 493b90e..061cd45 100644 --- a/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala +++ b/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala @@ -1,39 +1,46 @@ package eu.xeppaka.bot1 +import java.io.InputStream +import java.security.{KeyStore, SecureRandom} +import java.util.UUID + import akka.actor.ActorSystem -import akka.http.scaladsl.Http +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 javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} import scala.concurrent.{ExecutionContextExecutor, Future} import scala.io.StdIn -import TelegramEntities._ -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model.{HttpRequest, StatusCodes} -import akka.http.scaladsl.server.directives.LoggingMagnet -import akka.http.scaladsl.unmarshalling.Unmarshal -import spray.json._ - import scala.util.{Failure, Success} -trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { - implicit val getMeFormat: RootJsonFormat[GetMe] = jsonFormat4(GetMe) - implicit val responseFormat: RootJsonFormat[Response[GetMe]] = jsonFormat4(Response[GetMe]) -} +class TelegramBotServer(botId: String, port: Int, httpsContext: Option[HttpsConnectionContext])(implicit val actorSystem: ActorSystem) { -class TelegramBotServer extends JsonSupport { + import eu.xeppaka.bot1.TelegramEntities._ - def run(): Unit = { - implicit val actorSystem: ActorSystem = ActorSystem("telegram-bot") - implicit val materializer: ActorMaterializer = ActorMaterializer() - implicit val executionContext: ExecutionContextExecutor = actorSystem.dispatcher + private val botUri = BotUri(botId) + private implicit val materializer: ActorMaterializer = ActorMaterializer() + private implicit val executionContext: ExecutionContextExecutor = actorSystem.dispatcher - val bindingFuture = Http().bindAndHandle(botRoutes(), "localhost", 8080) + private val http: HttpExt = Http() + private val hookId = UUID.randomUUID().toString + private val webhookUri = Uri(s"https://xeppaka.eu:8443/$hookId") + private val bindingFuture = http.bindAndHandle(botRoutes(hookId), + "127.0.0.1", + port, + connectionContext = httpsContext.getOrElse(http.defaultClientHttpsContext)) - StdIn.readLine() + println(s"webhook path: $webhookUri") + def stop(): Unit = { bindingFuture + .andThen { case _ => http.shutdownAllConnectionPools() } .flatMap(_.unbind()) .onComplete(_ => actorSystem.terminate()) } @@ -43,11 +50,11 @@ class TelegramBotServer extends JsonSupport { println(res) } - def botRoutes()(implicit actorSystem: ActorSystem, materializer: ActorMaterializer, executionContext: ExecutionContextExecutor): Route = { - path("test") { - get { + def botRoutes(hookId: String): Route = { + path(hookId) { + post { logRequestResult(LoggingMagnet(_ => printRequestMethodAndResponseStatus)) { - onComplete(getBotInfo()) { + onComplete(getBotInfo) { case Success(res) => complete(res.ok.toString) case Failure(ex) => complete(StatusCodes.InternalServerError, "Boooom!") } @@ -56,17 +63,73 @@ class TelegramBotServer extends JsonSupport { } } - def getBotInfo()(implicit actorSystem: ActorSystem, materializer: ActorMaterializer, executionContext: ExecutionContextExecutor): Future[Response[GetMe]] = { - Http().singleRequest(HttpRequest(uri = "https://api.telegram.org/bot570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4/getMe")).flatMap(Unmarshal(_).to[Response[GetMe]]) + def getBotInfo: Future[Response[GetMe]] = { + http.singleRequest(HttpRequest(uri = botUri.getMe)).flatMap(Unmarshal(_).to[Response[GetMe]]) } - def setWebhook(): Unit = { + 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))) + // .flatMap(Unmarshal(_).to[Response[String]]) + } + def deleteWebhook(): Future[Response[String]] = { + http + .singleRequest(HttpRequest(uri = botUri.deleteWebhook, method = HttpMethods.POST)) + .flatMap(Unmarshal(_).to[Response[String]]) + } + + 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])(implicit actorSystem: ActorSystem): TelegramBotServer = new TelegramBotServer(botId, port, httpsContext)(actorSystem) + def main(args: Array[String]): Unit = { - new TelegramBotServer().run() + val httpsContext = createHttpsConnectionContext + + implicit val actorSystem: ActorSystem = ActorSystem("telegram-bot") + implicit val executionContext: ExecutionContextExecutor = actorSystem.dispatcher + + val tbs = TelegramBotServer(8443, Some(createHttpsConnectionContext)) + //tbs.setWebhook() + + tbs + .getWebhookInfo() + .onComplete(println(_)) + + StdIn.readLine() + + tbs.deleteWebhook() + .onComplete(r => tbs.stop()) + } + + def createHttpsConnectionContext: HttpsConnectionContext = { + val password: Array[Char] = "changeit".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") + + 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 index 30c4c11..2034199 100644 --- a/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala +++ b/src/main/scala/eu/xeppaka/bot1/TelegramEntities.scala @@ -1,10 +1,13 @@ package eu.xeppaka.bot1 -object TelegramEntities { +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import spray.json._ + +object TelegramEntities extends SprayJsonSupport with DefaultJsonProtocol { case class Response[T](ok: Boolean, - description: Option[String], - error_code: Option[Int], + description: Option[String] = None, + error_code: Option[Int] = None, result: T) case class GetMe(id: Int, is_bot: Boolean, first_name: String, username: String) @@ -19,29 +22,29 @@ object TelegramEntities { latitude: Float) case class Update(update_id: Int, - message: Option[Message], - edited_message: Option[Message], - channel_post: Option[Message], - edited_channel_post: Option[Message], - inline_query: Option[InlineQuery], - chosen_inline_result: Option[ChosenInlineResult], - callback_query: Option[CallbackQuery], - shipping_query: Option[ShippingQuery], - pre_checkout_query: Option[PreCheckoutQuery]) + 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], - inline_message_id: Option[String], + location: Option[Location] = None, + inline_message_id: Option[String] = None, query: String) case class CallbackQuery(id: String, from: User, - message: Option[Message], - inline_message_id: Option[String], + message: Option[Message] = None, + inline_message_id: Option[String] = None, chat_instance: String, - data: Option[String], - game_short_name: Option[String]) + data: Option[String] = None, + game_short_name: Option[String] = None) case class ShippingQuery(id: String, from: User, @@ -60,20 +63,20 @@ object TelegramEntities { currency: String, total_amount: Int, invoice_payload: String, - shipping_option_id: Option[String], - order_info: Option[OrderInfo]) + shipping_option_id: Option[String] = None, + order_info: Option[OrderInfo] = None) - case class OrderInfo(name: Option[String], - phone_number: Option[String], - email: Option[String], - shipping_address: Option[ShippingAddress]) + 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], - username: Option[String], - language_code: Option[String]) + last_name: Option[String] = None, + username: Option[String] = None, + language_code: Option[String] = None) case class Message() @@ -81,23 +84,39 @@ object TelegramEntities { case class Chat(id: Int, `type`: String, - title: Option[String], - username: Option[String], - first_name: Option[String], - last_name: Option[String], - all_members_are_administrators: Option[Boolean], - photo: Option[ChatPhoto], - description: Option[String], - invite_link: Option[String], - pinned_message: Option[Message], - sticker_set_name: Option[String], - can_set_sticker_set: Option[Boolean] - ) + 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 InputFile() case class Webhook(url: String, - certificate: Option[InputFile], - max_connections: Option[Int], - allowed_updates: Option[Seq[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) + + implicit val inputFileFormat: RootJsonFormat[InputFile] = jsonFormat0(InputFile) + implicit val webHookFormat: RootJsonFormat[Webhook] = jsonFormat4(Webhook) + implicit val webHookInfoFormat: RootJsonFormat[WebhookInfo] = jsonFormat7(WebhookInfo) + implicit val getMeFormat: RootJsonFormat[GetMe] = jsonFormat4(GetMe) + // responses + 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]) }