Refactored telegram bot server
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
akka {
|
||||
loglevel = "INFO"
|
||||
}
|
||||
Binary file not shown.
@@ -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-----
|
||||
@@ -1,19 +0,0 @@
|
||||
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 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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user