diff --git a/build.mill b/build.mill index 33ec4d8a..98ba2ab0 100644 --- a/build.mill +++ b/build.mill @@ -1,6 +1,7 @@ import mill._ import mill.scalalib._ import mill.scalalib.publish._ +import mill.scalajslib._ import mill.javalib.api.JvmWorkerUtil val ScalaVersions = Seq("2.12.21", "2.13.18", "3.3.8") @@ -19,6 +20,7 @@ object library { val sttp = "4.0.26" val scalaTest = "3.2.20" val scalaMockScalaTest = "7.5.5" + val scalaJS = "1.22.0" val scalaLogging = "3.9.6" val logback = "1.5.38" val scalajHttp = "2.4.2" @@ -95,8 +97,7 @@ trait Bot4sTelegramModule extends CrossScalaModule { library.circeLiteral, library.catsCore, library.catsFree, - library.sttpCore, - library.scalaLogging + library.sttpCore ) trait Tests extends ScalaTests with TestModule.ScalaTest { @@ -158,12 +159,20 @@ object core extends Module { } override def mvnDeps = Task { - super.mvnDeps() ++ versionDependencies() + super.mvnDeps() ++ Seq(library.scalaLogging) ++ versionDependencies() } object test extends Tests } + object js extends Cross[CoreJsModule](ScalaVersions) + trait CoreJsModule extends Bot4sTelegramCrossPlatform with ScalaJSModule with Publishable { + override val platformSegment: String = "js" + override val location: String = "core" + + override def scalaJSVersion = library.Version.scalaJS + } + } object pekko extends Module { @@ -176,6 +185,7 @@ object pekko extends Module { override def mvnDeps = Task { super.mvnDeps() ++ Seq( + library.scalaLogging, library.pekkoActor, library.pekkoHttp, library.pekkoStream diff --git a/core/src-js/com/bot4s/telegram/clients/SttpInputFile.scala b/core/src-js/com/bot4s/telegram/clients/SttpInputFile.scala new file mode 100644 index 00000000..366d208c --- /dev/null +++ b/core/src-js/com/bot4s/telegram/clients/SttpInputFile.scala @@ -0,0 +1,14 @@ +package com.bot4s.telegram.clients + +import com.bot4s.telegram.models.InputFile +import sttp.client4._ +import sttp.model.Part + +private[clients] object SttpInputFile { + def multipartBodyPart(key: String, inputFile: InputFile): Option[Part[BasicBodyPart]] = + inputFile match { + // FileId is submitted through the JSON fields, see `inputFileEncoder`. + case InputFile.FileId(_) => None + case InputFile.Contents(filename, contents) => Some(multipart(key, contents).fileName(filename)) + } +} diff --git a/core/src-js/com/bot4s/telegram/models/InputFile.scala b/core/src-js/com/bot4s/telegram/models/InputFile.scala new file mode 100644 index 00000000..96de9e73 --- /dev/null +++ b/core/src-js/com/bot4s/telegram/models/InputFile.scala @@ -0,0 +1,25 @@ +package com.bot4s.telegram.models + +import io.circe.Encoder +import io.circe.syntax.EncoderOps + +/** + * This object represents the contents of a file to be uploaded. + * + * Scala.js builds support file_id reuse and in-memory contents values in the model. + * Filesystem paths are JVM-only. + */ +trait InputFile + +object InputFile { + final case class FileId(fileId: String) extends InputFile + final case class Contents(filename: String, contents: Array[Byte]) extends InputFile + + def apply(fileId: String): InputFile = FileId(fileId) + def apply(filename: String, contents: Array[Byte]): InputFile = Contents(filename, contents) + + implicit val circeEncoder: Encoder[InputFile] = Encoder.instance { + case InputFile.FileId(fileId) => fileId.asJson + case _ => io.circe.Json.Null + } +} diff --git a/core/src-js/com/bot4s/telegram/util/StrictLogging.scala b/core/src-js/com/bot4s/telegram/util/StrictLogging.scala new file mode 100644 index 00000000..e06e81ae --- /dev/null +++ b/core/src-js/com/bot4s/telegram/util/StrictLogging.scala @@ -0,0 +1,21 @@ +package com.bot4s.telegram.util + +trait StrictLogging { + protected lazy val logger: BotLogger = NoopBotLogger +} + +trait BotLogger { + def trace(message: String, args: Any*): Unit + def debug(message: String, args: Any*): Unit + def info(message: String, args: Any*): Unit + def warn(message: String, args: Any*): Unit + def error(message: String, args: Any*): Unit +} + +private[telegram] object NoopBotLogger extends BotLogger { + override def trace(message: String, args: Any*): Unit = () + override def debug(message: String, args: Any*): Unit = () + override def info(message: String, args: Any*): Unit = () + override def warn(message: String, args: Any*): Unit = () + override def error(message: String, args: Any*): Unit = () +} diff --git a/core/src-jvm-2/com/bot4s/telegram/clients/ScalajHttpClient.scala b/core/src-jvm-2/com/bot4s/telegram/clients/ScalajHttpClient.scala index e39da46c..7498c685 100644 --- a/core/src-jvm-2/com/bot4s/telegram/clients/ScalajHttpClient.scala +++ b/core/src-jvm-2/com/bot4s/telegram/clients/ScalajHttpClient.scala @@ -11,7 +11,6 @@ import com.bot4s.telegram.marshalling import io.circe.parser.parse import io.circe.{ Decoder, Encoder } import scalaj.http.{ Http, MultiPart } -import com.typesafe.scalalogging.StrictLogging import scala.concurrent.{ blocking, ExecutionContext, Future } @@ -29,8 +28,7 @@ import scala.concurrent.{ blocking, ExecutionContext, Future } */ class ScalajHttpClient(token: String, proxy: Proxy = Proxy.NO_PROXY, telegramHost: String = "api.telegram.org")(implicit ec: ExecutionContext -) extends RequestHandler[Future] - with StrictLogging { +) extends RequestHandler[Future] { val connectionTimeoutMs = 10000 val readTimeoutMs = 50000 diff --git a/core/src-jvm/com/bot4s/telegram/clients/SttpInputFile.scala b/core/src-jvm/com/bot4s/telegram/clients/SttpInputFile.scala new file mode 100644 index 00000000..4391780e --- /dev/null +++ b/core/src-jvm/com/bot4s/telegram/clients/SttpInputFile.scala @@ -0,0 +1,15 @@ +package com.bot4s.telegram.clients + +import com.bot4s.telegram.models.InputFile +import sttp.client4._ +import sttp.model.Part + +private[clients] object SttpInputFile { + def multipartBodyPart(key: String, inputFile: InputFile): Option[Part[BasicBodyPart]] = + inputFile match { + // FileId is submitted through the JSON fields, see `inputFileEncoder`. + case InputFile.FileId(_) => None + case InputFile.Contents(filename, contents) => Some(multipart(key, contents).fileName(filename)) + case InputFile.Path(path) => Some(multipartFile(key, path)) + } +} diff --git a/core/src/com/bot4s/telegram/models/InputFile.scala b/core/src-jvm/com/bot4s/telegram/models/InputFile.scala similarity index 100% rename from core/src/com/bot4s/telegram/models/InputFile.scala rename to core/src-jvm/com/bot4s/telegram/models/InputFile.scala diff --git a/core/src-jvm/com/bot4s/telegram/util/StrictLogging.scala b/core/src-jvm/com/bot4s/telegram/util/StrictLogging.scala new file mode 100644 index 00000000..423926ed --- /dev/null +++ b/core/src-jvm/com/bot4s/telegram/util/StrictLogging.scala @@ -0,0 +1,3 @@ +package com.bot4s.telegram.util + +trait StrictLogging extends com.typesafe.scalalogging.StrictLogging diff --git a/core/src/com/bot4s/telegram/api/Polling.scala b/core/src/com/bot4s/telegram/api/Polling.scala index 86e3edc6..e1ccb21a 100644 --- a/core/src/com/bot4s/telegram/api/Polling.scala +++ b/core/src/com/bot4s/telegram/api/Polling.scala @@ -1,7 +1,7 @@ package com.bot4s.telegram.api import com.bot4s.telegram.methods.GetUpdates -import com.typesafe.scalalogging.StrictLogging +import com.bot4s.telegram.util.StrictLogging import scala.concurrent.duration.{ Duration, _ } import com.bot4s.telegram.models.ParsedUpdate diff --git a/core/src/com/bot4s/telegram/api/RequestHandler.scala b/core/src/com/bot4s/telegram/api/RequestHandler.scala index 4a65518e..c4068fd4 100644 --- a/core/src/com/bot4s/telegram/api/RequestHandler.scala +++ b/core/src/com/bot4s/telegram/api/RequestHandler.scala @@ -8,7 +8,7 @@ import cats.syntax.functor._ import cats.syntax.monadError._ import com.bot4s.telegram.methods._ import io.circe.{ Decoder, Encoder } -import com.typesafe.scalalogging.StrictLogging +import com.bot4s.telegram.util.StrictLogging import io.circe.Decoder._ import com.bot4s.telegram.marshalling._ diff --git a/core/src/com/bot4s/telegram/cats/Polling.scala b/core/src/com/bot4s/telegram/cats/Polling.scala index c917a327..1d9118a4 100644 --- a/core/src/com/bot4s/telegram/cats/Polling.scala +++ b/core/src/com/bot4s/telegram/cats/Polling.scala @@ -8,7 +8,7 @@ import cats.syntax.traverse._ import com.bot4s.telegram.api.{ Polling => BasePolling } import com.bot4s.telegram.methods.{ DeleteWebhook, GetMe } import com.bot4s.telegram.models.User -import com.typesafe.scalalogging.StrictLogging +import com.bot4s.telegram.util.StrictLogging import com.bot4s.telegram.models.ParsedUpdate case class PollingState(botUser: User, offset: Option[Long]) diff --git a/core/src/com/bot4s/telegram/clients/SttpClient.scala b/core/src/com/bot4s/telegram/clients/SttpClient.scala index fbdfe3eb..0ff8d3bb 100644 --- a/core/src/com/bot4s/telegram/clients/SttpClient.scala +++ b/core/src/com/bot4s/telegram/clients/SttpClient.scala @@ -7,11 +7,8 @@ import com.bot4s.telegram.api.RequestHandler import com.bot4s.telegram.marshalling.CaseConversions import com.bot4s.telegram.methods.{ JsonRequest, MultipartRequest, Request => BotRequest, Response } import com.bot4s.telegram.marshalling -import com.bot4s.telegram.models.InputFile import io.circe.parser.parse import io.circe.{ Decoder, Encoder } -import com.typesafe.scalalogging.StrictLogging - import scala.concurrent.duration._ import sttp.client4._ @@ -29,8 +26,7 @@ import sttp.client4.{ Request => SttpRequest } class SttpClient[F[_]](token: String, telegramHost: String = "api.telegram.org")(implicit backend: Backend[F], monadError: MonadError[F, Throwable] -) extends RequestHandler[F]()(using monadError) - with StrictLogging { +) extends RequestHandler[F]()(using monadError) { val readTimeout: Duration = 50.seconds @@ -54,13 +50,7 @@ class SttpClient[F[_]](token: String, telegramHost: String = "api.telegram.org") case r: MultipartRequest => val parts = r.getFiles.flatMap { case (camelKey, inputFile) => val key = CaseConversions.snakenize(camelKey) - inputFile match { - // FileId must be submitted through the JSON query, see `inputFileEncoder` - case InputFile.FileId(id) => None - case InputFile.Contents(filename, contents) => Some(multipart(key, contents).fileName(filename)) - case InputFile.Path(path) => Some(multipartFile(key, path)) - case other => throw new RuntimeException(s"InputFile $other not supported") - } + SttpInputFile.multipartBodyPart(key, inputFile) } val fields = parse(marshalling.toJson(request)) diff --git a/core/src/com/bot4s/telegram/future/Polling.scala b/core/src/com/bot4s/telegram/future/Polling.scala index 11faedd7..56e513f3 100644 --- a/core/src/com/bot4s/telegram/future/Polling.scala +++ b/core/src/com/bot4s/telegram/future/Polling.scala @@ -3,7 +3,7 @@ package com.bot4s.telegram.future import com.bot4s.telegram.api.{ Polling => BasePolling } import com.bot4s.telegram.methods.{ DeleteWebhook, GetMe } import com.bot4s.telegram.models.{ ParsedUpdate, User } -import com.typesafe.scalalogging.StrictLogging +import com.bot4s.telegram.util.StrictLogging import scala.concurrent.Future import scala.util.control.NonFatal diff --git a/core/src/com/bot4s/telegram/marshalling/CirceDecoders.scala b/core/src/com/bot4s/telegram/marshalling/CirceDecoders.scala index 2c3ea9ef..eb24401e 100644 --- a/core/src/com/bot4s/telegram/marshalling/CirceDecoders.scala +++ b/core/src/com/bot4s/telegram/marshalling/CirceDecoders.scala @@ -19,7 +19,7 @@ import com.bot4s.telegram.models.StickerFormat.StickerFormat import UpdateType.UpdateType import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder -import com.typesafe.scalalogging.StrictLogging +import com.bot4s.telegram.util.StrictLogging /** * Circe marshalling borrowed/inspired from [[https://github.com/nikdon/telepooz]] diff --git a/pekko/src/com/bot4s/telegram/clients/PekkoHttpClient.scala b/pekko/src/com/bot4s/telegram/clients/PekkoHttpClient.scala index ae2173fd..e03aec43 100644 --- a/pekko/src/com/bot4s/telegram/clients/PekkoHttpClient.scala +++ b/pekko/src/com/bot4s/telegram/clients/PekkoHttpClient.scala @@ -12,7 +12,6 @@ import com.bot4s.telegram.marshalling.PekkoHttpMarshalling import com.bot4s.telegram.marshalling._ import com.bot4s.telegram.methods.{ Request, Response } import io.circe.{ Decoder, Encoder } -import com.typesafe.scalalogging.StrictLogging import scala.concurrent.{ ExecutionContext, Future } @@ -26,8 +25,7 @@ class PekkoHttpClient(token: String, telegramHost: String = "api.telegram.org")( system: ActorSystem, materializer: Materializer, ec: ExecutionContext -) extends RequestHandler[Future] - with StrictLogging { +) extends RequestHandler[Future] { import PekkoHttpMarshalling._ private val apiBaseUrl = s"https://$telegramHost/bot$token/" diff --git a/pekko/src/com/bot4s/telegram/clients/YetAnotherPekkoClient.scala b/pekko/src/com/bot4s/telegram/clients/YetAnotherPekkoClient.scala index d3401379..6a8155fd 100644 --- a/pekko/src/com/bot4s/telegram/clients/YetAnotherPekkoClient.scala +++ b/pekko/src/com/bot4s/telegram/clients/YetAnotherPekkoClient.scala @@ -12,7 +12,6 @@ import cats.instances.future._ import com.bot4s.telegram.api.RequestHandler import com.bot4s.telegram.methods.{ Request, Response } import io.circe.{ Decoder, Encoder } -import com.typesafe.scalalogging.StrictLogging import com.bot4s.telegram.marshalling.responseDecoder import scala.concurrent.{ ExecutionContext, Future } @@ -21,8 +20,7 @@ class YetAnotherPekkoClient(token: String, telegramHost: String = "api.telegram. system: ActorSystem, materializer: Materializer, ec: ExecutionContext -) extends RequestHandler[Future] - with StrictLogging { +) extends RequestHandler[Future] { private val flow = Http().outgoingConnectionHttps(telegramHost)