Files
telegram-bot-delivery/src/main/scala/eu/xeppaka/bot1/TelegramBotServer.scala
2018-05-20 22:50:31 +02:00

176 lines
6.7 KiB
Scala

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:$port/$hookId")
private val bindingFuture = http.bindAndHandle(botRoutes(hookId),
"pkcloud",
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]) { 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(88, 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)
}
}