From 5777c0b3ef22d09dcf0cbfc56f5a14b64d5cecdc Mon Sep 17 00:00:00 2001 From: ex0ns Date: Wed, 15 Jul 2026 23:36:22 +0200 Subject: [PATCH 1/5] Add Scala.js core target --- build.sc | 20 ++++++++-- .../com/bot4s/telegram/models/InputFile.scala | 25 ++++++++++++ .../bot4s/telegram/util/PlatformLogger.scala | 5 +++ .../telegram/clients/ScalajHttpClient.scala | 4 +- .../com/bot4s/telegram/cats/TelegramBot.scala | 0 .../telegram/clients/FutureSttpClient.scala | 0 .../bot4s/telegram/clients/SttpClient.scala | 6 +-- .../com/bot4s/telegram/models/InputFile.scala | 0 .../bot4s/telegram/util/PlatformLogger.scala | 38 +++++++++++++++++++ core/src/com/bot4s/telegram/api/Polling.scala | 2 +- .../bot4s/telegram/api/RequestHandler.scala | 15 ++++++-- .../src/com/bot4s/telegram/cats/Polling.scala | 2 +- .../com/bot4s/telegram/future/Polling.scala | 2 +- .../telegram/marshalling/CirceDecoders.scala | 2 +- .../com/bot4s/telegram/methods/Request.scala | 7 +++- .../bot4s/telegram/util/StrictLogging.scala | 21 ++++++++++ .../telegram/clients/PekkoHttpClient.scala | 4 +- .../clients/YetAnotherPekkoClient.scala | 4 +- 18 files changed, 129 insertions(+), 28 deletions(-) create mode 100644 core/src-js/com/bot4s/telegram/models/InputFile.scala create mode 100644 core/src-js/com/bot4s/telegram/util/PlatformLogger.scala rename core/{src => src-jvm}/com/bot4s/telegram/cats/TelegramBot.scala (100%) rename core/{src => src-jvm}/com/bot4s/telegram/clients/FutureSttpClient.scala (100%) rename core/{src => src-jvm}/com/bot4s/telegram/clients/SttpClient.scala (95%) rename core/{src => src-jvm}/com/bot4s/telegram/models/InputFile.scala (100%) create mode 100644 core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala create mode 100644 core/src/com/bot4s/telegram/util/StrictLogging.scala diff --git a/build.sc b/build.sc index 70889e90..e9893560 100644 --- a/build.sc +++ b/build.sc @@ -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.20", "2.13.18", "3.3.7") @@ -19,6 +20,7 @@ object library { val sttp = "4.0.13" val scalaTest = "3.2.19" val scalaMockScalaTest = "7.4.0" + val scalaJS = "1.22.0" val scalaLogging = "3.9.5" val logback = "1.5.18" val scalajHttp = "2.4.2" @@ -94,9 +96,7 @@ trait Bot4sTelegramModule extends CrossScalaModule { library.circeParser, library.circeLiteral, library.catsCore, - library.catsFree, - library.sttpCore, - library.scalaLogging + library.catsFree ) trait Tests extends ScalaTests with TestModule.ScalaTest { @@ -158,12 +158,23 @@ object core extends Module { } override def mvnDeps = Task { - super.mvnDeps() ++ versionDependencies() + super.mvnDeps() ++ Seq( + library.scalaLogging, + library.sttpCore + ) ++ 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 +187,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/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/PlatformLogger.scala b/core/src-js/com/bot4s/telegram/util/PlatformLogger.scala new file mode 100644 index 00000000..1ba2432f --- /dev/null +++ b/core/src-js/com/bot4s/telegram/util/PlatformLogger.scala @@ -0,0 +1,5 @@ +package com.bot4s.telegram.util + +private[telegram] object PlatformLogger { + def create(owner: AnyRef): BotLogger = NoopBotLogger +} 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/com/bot4s/telegram/cats/TelegramBot.scala b/core/src-jvm/com/bot4s/telegram/cats/TelegramBot.scala similarity index 100% rename from core/src/com/bot4s/telegram/cats/TelegramBot.scala rename to core/src-jvm/com/bot4s/telegram/cats/TelegramBot.scala diff --git a/core/src/com/bot4s/telegram/clients/FutureSttpClient.scala b/core/src-jvm/com/bot4s/telegram/clients/FutureSttpClient.scala similarity index 100% rename from core/src/com/bot4s/telegram/clients/FutureSttpClient.scala rename to core/src-jvm/com/bot4s/telegram/clients/FutureSttpClient.scala diff --git a/core/src/com/bot4s/telegram/clients/SttpClient.scala b/core/src-jvm/com/bot4s/telegram/clients/SttpClient.scala similarity index 95% rename from core/src/com/bot4s/telegram/clients/SttpClient.scala rename to core/src-jvm/com/bot4s/telegram/clients/SttpClient.scala index fbdfe3eb..e6bd9a50 100644 --- a/core/src/com/bot4s/telegram/clients/SttpClient.scala +++ b/core/src-jvm/com/bot4s/telegram/clients/SttpClient.scala @@ -10,8 +10,6 @@ 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._ @@ -22,15 +20,13 @@ import sttp.client4.{ Request => SttpRequest } /** * Sttp HTTP client. - * Supports browsers via sttp's FetchBackend. * * @param token Bot token */ 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 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/PlatformLogger.scala b/core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala new file mode 100644 index 00000000..5786ff20 --- /dev/null +++ b/core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala @@ -0,0 +1,38 @@ +package com.bot4s.telegram.util + +private[telegram] object PlatformLogger { + def create(owner: AnyRef): BotLogger = + new Slf4jBotLogger(com.typesafe.scalalogging.Logger(owner.getClass)) +} + +private final class Slf4jBotLogger(delegate: com.typesafe.scalalogging.Logger) extends BotLogger { + override def trace(message: String, args: Any*): Unit = args match { + case Seq(cause: Throwable) => delegate.trace(message, cause) + case other => delegate.trace(format(message, other)) + } + + override def debug(message: String, args: Any*): Unit = args match { + case Seq(cause: Throwable) => delegate.debug(message, cause) + case other => delegate.debug(format(message, other)) + } + + override def info(message: String, args: Any*): Unit = args match { + case Seq(cause: Throwable) => delegate.info(message, cause) + case other => delegate.info(format(message, other)) + } + + override def warn(message: String, args: Any*): Unit = args match { + case Seq(cause: Throwable) => delegate.warn(message, cause) + case other => delegate.warn(format(message, other)) + } + + override def error(message: String, args: Any*): Unit = args match { + case Seq(cause: Throwable) => delegate.error(message, cause) + case other => delegate.error(format(message, other)) + } + + private def format(message: String, args: Seq[Any]): String = + args.foldLeft(message) { (acc, arg) => + acc.replaceFirst("\\{\\}", java.util.regex.Matcher.quoteReplacement(String.valueOf(arg))) + } +} 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..36dfce9e 100644 --- a/core/src/com/bot4s/telegram/api/RequestHandler.scala +++ b/core/src/com/bot4s/telegram/api/RequestHandler.scala @@ -1,14 +1,12 @@ package com.bot4s.telegram.api -import java.util.UUID - import cats.MonadError import cats.syntax.flatMap._ 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._ @@ -27,7 +25,7 @@ abstract class RequestHandler[F[_]](implicit monadError: MonadError[F, Throwable def apply[T <: Request: Encoder](request: T)(implicit d: Decoder[request.Response]): F[request.Response] = for { uuid <- monadError.pure { - val uuid = UUID.randomUUID() + val uuid = RequestHandler.nextRequestId() logger.trace("REQUEST {} {}", uuid, request) uuid } @@ -56,3 +54,12 @@ abstract class RequestHandler[F[_]](implicit monadError: MonadError[F, Throwable throw new RuntimeException(s"Unexpected API response: $other") } } + +object RequestHandler { + private[this] var requestId = 0L + + private def nextRequestId(): Long = synchronized { + requestId += 1 + requestId + } +} 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/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/core/src/com/bot4s/telegram/methods/Request.scala b/core/src/com/bot4s/telegram/methods/Request.scala index 42536113..d498bc0f 100644 --- a/core/src/com/bot4s/telegram/methods/Request.scala +++ b/core/src/com/bot4s/telegram/methods/Request.scala @@ -24,7 +24,10 @@ trait Request { type Response /** - * Extract request URL from class name. + * Extract request URL from the case class or case object name. */ - def methodName: String = getClass.getSimpleName.takeWhile('$' != _) + def methodName: String = this match { + case product: Product => product.productPrefix + case _ => throw new IllegalStateException("Request.methodName must be overridden for non-product requests") + } } diff --git a/core/src/com/bot4s/telegram/util/StrictLogging.scala b/core/src/com/bot4s/telegram/util/StrictLogging.scala new file mode 100644 index 00000000..c3f63344 --- /dev/null +++ b/core/src/com/bot4s/telegram/util/StrictLogging.scala @@ -0,0 +1,21 @@ +package com.bot4s.telegram.util + +trait StrictLogging { + protected lazy val logger: BotLogger = PlatformLogger.create(this) +} + +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/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) From 35b1efb8bb78a3b6b3904399566ab758ea3c1f02 Mon Sep 17 00:00:00 2001 From: ex0ns Date: Thu, 16 Jul 2026 10:54:15 +0200 Subject: [PATCH 2/5] Use UUID request ids --- .../bot4s/telegram/api/RequestHandler.scala | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/core/src/com/bot4s/telegram/api/RequestHandler.scala b/core/src/com/bot4s/telegram/api/RequestHandler.scala index 36dfce9e..964955d0 100644 --- a/core/src/com/bot4s/telegram/api/RequestHandler.scala +++ b/core/src/com/bot4s/telegram/api/RequestHandler.scala @@ -24,18 +24,18 @@ abstract class RequestHandler[F[_]](implicit monadError: MonadError[F, Throwable */ def apply[T <: Request: Encoder](request: T)(implicit d: Decoder[request.Response]): F[request.Response] = for { - uuid <- monadError.pure { - val uuid = RequestHandler.nextRequestId() - logger.trace("REQUEST {} {}", uuid, request) - uuid - } + requestId <- monadError.pure { + val requestId = RequestHandler.nextRequestId() + logger.trace("REQUEST {} {}", requestId, request) + requestId + } result <- monadError .attempt(sendRequest(request)) .flatTap { case Right(response) => - monadError.pure(logger.trace("RESPONSE {} {}", uuid, response)) + monadError.pure(logger.trace("RESPONSE {} {}", requestId, response)) case Left(e) => - monadError.pure(logger.error("RESPONSE {} {}", uuid, e)) + monadError.pure(logger.error("RESPONSE {} {}", requestId, e)) } .rethrow } yield result @@ -56,10 +56,5 @@ abstract class RequestHandler[F[_]](implicit monadError: MonadError[F, Throwable } object RequestHandler { - private[this] var requestId = 0L - - private def nextRequestId(): Long = synchronized { - requestId += 1 - requestId - } + private def nextRequestId(): String = java.util.UUID.randomUUID().toString } From 7969471789b1920602a931536f5c0e76d9418623 Mon Sep 17 00:00:00 2001 From: ex0ns Date: Thu, 16 Jul 2026 11:57:00 +0200 Subject: [PATCH 3/5] simplify logging, revert request changes --- .../bot4s/telegram/util/PlatformLogger.scala | 5 --- .../bot4s/telegram/util/StrictLogging.scala | 2 +- .../bot4s/telegram/util/PlatformLogger.scala | 38 ------------------- .../bot4s/telegram/util/StrictLogging.scala | 3 ++ .../com/bot4s/telegram/methods/Request.scala | 7 +--- 5 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 core/src-js/com/bot4s/telegram/util/PlatformLogger.scala rename core/{src => src-js}/com/bot4s/telegram/util/StrictLogging.scala (90%) delete mode 100644 core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala create mode 100644 core/src-jvm/com/bot4s/telegram/util/StrictLogging.scala diff --git a/core/src-js/com/bot4s/telegram/util/PlatformLogger.scala b/core/src-js/com/bot4s/telegram/util/PlatformLogger.scala deleted file mode 100644 index 1ba2432f..00000000 --- a/core/src-js/com/bot4s/telegram/util/PlatformLogger.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.bot4s.telegram.util - -private[telegram] object PlatformLogger { - def create(owner: AnyRef): BotLogger = NoopBotLogger -} diff --git a/core/src/com/bot4s/telegram/util/StrictLogging.scala b/core/src-js/com/bot4s/telegram/util/StrictLogging.scala similarity index 90% rename from core/src/com/bot4s/telegram/util/StrictLogging.scala rename to core/src-js/com/bot4s/telegram/util/StrictLogging.scala index c3f63344..e06e81ae 100644 --- a/core/src/com/bot4s/telegram/util/StrictLogging.scala +++ b/core/src-js/com/bot4s/telegram/util/StrictLogging.scala @@ -1,7 +1,7 @@ package com.bot4s.telegram.util trait StrictLogging { - protected lazy val logger: BotLogger = PlatformLogger.create(this) + protected lazy val logger: BotLogger = NoopBotLogger } trait BotLogger { diff --git a/core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala b/core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala deleted file mode 100644 index 5786ff20..00000000 --- a/core/src-jvm/com/bot4s/telegram/util/PlatformLogger.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.bot4s.telegram.util - -private[telegram] object PlatformLogger { - def create(owner: AnyRef): BotLogger = - new Slf4jBotLogger(com.typesafe.scalalogging.Logger(owner.getClass)) -} - -private final class Slf4jBotLogger(delegate: com.typesafe.scalalogging.Logger) extends BotLogger { - override def trace(message: String, args: Any*): Unit = args match { - case Seq(cause: Throwable) => delegate.trace(message, cause) - case other => delegate.trace(format(message, other)) - } - - override def debug(message: String, args: Any*): Unit = args match { - case Seq(cause: Throwable) => delegate.debug(message, cause) - case other => delegate.debug(format(message, other)) - } - - override def info(message: String, args: Any*): Unit = args match { - case Seq(cause: Throwable) => delegate.info(message, cause) - case other => delegate.info(format(message, other)) - } - - override def warn(message: String, args: Any*): Unit = args match { - case Seq(cause: Throwable) => delegate.warn(message, cause) - case other => delegate.warn(format(message, other)) - } - - override def error(message: String, args: Any*): Unit = args match { - case Seq(cause: Throwable) => delegate.error(message, cause) - case other => delegate.error(format(message, other)) - } - - private def format(message: String, args: Seq[Any]): String = - args.foldLeft(message) { (acc, arg) => - acc.replaceFirst("\\{\\}", java.util.regex.Matcher.quoteReplacement(String.valueOf(arg))) - } -} 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/methods/Request.scala b/core/src/com/bot4s/telegram/methods/Request.scala index d498bc0f..42536113 100644 --- a/core/src/com/bot4s/telegram/methods/Request.scala +++ b/core/src/com/bot4s/telegram/methods/Request.scala @@ -24,10 +24,7 @@ trait Request { type Response /** - * Extract request URL from the case class or case object name. + * Extract request URL from class name. */ - def methodName: String = this match { - case product: Product => product.productPrefix - case _ => throw new IllegalStateException("Request.methodName must be overridden for non-product requests") - } + def methodName: String = getClass.getSimpleName.takeWhile('$' != _) } From bdecbb3df74d5c7141a58fe6a9cb8ddcfbce6b41 Mon Sep 17 00:00:00 2001 From: ex0ns Date: Thu, 16 Jul 2026 12:05:19 +0200 Subject: [PATCH 4/5] remove companion object --- .../bot4s/telegram/api/RequestHandler.scala | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/core/src/com/bot4s/telegram/api/RequestHandler.scala b/core/src/com/bot4s/telegram/api/RequestHandler.scala index 964955d0..c4068fd4 100644 --- a/core/src/com/bot4s/telegram/api/RequestHandler.scala +++ b/core/src/com/bot4s/telegram/api/RequestHandler.scala @@ -1,5 +1,7 @@ package com.bot4s.telegram.api +import java.util.UUID + import cats.MonadError import cats.syntax.flatMap._ import cats.syntax.functor._ @@ -24,18 +26,18 @@ abstract class RequestHandler[F[_]](implicit monadError: MonadError[F, Throwable */ def apply[T <: Request: Encoder](request: T)(implicit d: Decoder[request.Response]): F[request.Response] = for { - requestId <- monadError.pure { - val requestId = RequestHandler.nextRequestId() - logger.trace("REQUEST {} {}", requestId, request) - requestId - } + uuid <- monadError.pure { + val uuid = UUID.randomUUID() + logger.trace("REQUEST {} {}", uuid, request) + uuid + } result <- monadError .attempt(sendRequest(request)) .flatTap { case Right(response) => - monadError.pure(logger.trace("RESPONSE {} {}", requestId, response)) + monadError.pure(logger.trace("RESPONSE {} {}", uuid, response)) case Left(e) => - monadError.pure(logger.error("RESPONSE {} {}", requestId, e)) + monadError.pure(logger.error("RESPONSE {} {}", uuid, e)) } .rethrow } yield result @@ -54,7 +56,3 @@ abstract class RequestHandler[F[_]](implicit monadError: MonadError[F, Throwable throw new RuntimeException(s"Unexpected API response: $other") } } - -object RequestHandler { - private def nextRequestId(): String = java.util.UUID.randomUUID().toString -} From 6b4b6a4bf34fb07382a54b928ac13b57608b25b7 Mon Sep 17 00:00:00 2001 From: ex0ns Date: Thu, 16 Jul 2026 14:03:05 +0200 Subject: [PATCH 5/5] restore sttp client on scala.js --- build.sc | 8 +++----- .../bot4s/telegram/clients/SttpInputFile.scala | 14 ++++++++++++++ .../bot4s/telegram/clients/SttpInputFile.scala | 15 +++++++++++++++ .../com/bot4s/telegram/cats/TelegramBot.scala | 0 .../bot4s/telegram/clients/FutureSttpClient.scala | 0 .../com/bot4s/telegram/clients/SttpClient.scala | 10 ++-------- 6 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 core/src-js/com/bot4s/telegram/clients/SttpInputFile.scala create mode 100644 core/src-jvm/com/bot4s/telegram/clients/SttpInputFile.scala rename core/{src-jvm => src}/com/bot4s/telegram/cats/TelegramBot.scala (100%) rename core/{src-jvm => src}/com/bot4s/telegram/clients/FutureSttpClient.scala (100%) rename core/{src-jvm => src}/com/bot4s/telegram/clients/SttpClient.scala (83%) diff --git a/build.sc b/build.sc index e9893560..75553b02 100644 --- a/build.sc +++ b/build.sc @@ -96,7 +96,8 @@ trait Bot4sTelegramModule extends CrossScalaModule { library.circeParser, library.circeLiteral, library.catsCore, - library.catsFree + library.catsFree, + library.sttpCore ) trait Tests extends ScalaTests with TestModule.ScalaTest { @@ -158,10 +159,7 @@ object core extends Module { } override def mvnDeps = Task { - super.mvnDeps() ++ Seq( - library.scalaLogging, - library.sttpCore - ) ++ versionDependencies() + super.mvnDeps() ++ Seq(library.scalaLogging) ++ versionDependencies() } object test extends Tests 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-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-jvm/com/bot4s/telegram/cats/TelegramBot.scala b/core/src/com/bot4s/telegram/cats/TelegramBot.scala similarity index 100% rename from core/src-jvm/com/bot4s/telegram/cats/TelegramBot.scala rename to core/src/com/bot4s/telegram/cats/TelegramBot.scala diff --git a/core/src-jvm/com/bot4s/telegram/clients/FutureSttpClient.scala b/core/src/com/bot4s/telegram/clients/FutureSttpClient.scala similarity index 100% rename from core/src-jvm/com/bot4s/telegram/clients/FutureSttpClient.scala rename to core/src/com/bot4s/telegram/clients/FutureSttpClient.scala diff --git a/core/src-jvm/com/bot4s/telegram/clients/SttpClient.scala b/core/src/com/bot4s/telegram/clients/SttpClient.scala similarity index 83% rename from core/src-jvm/com/bot4s/telegram/clients/SttpClient.scala rename to core/src/com/bot4s/telegram/clients/SttpClient.scala index e6bd9a50..0ff8d3bb 100644 --- a/core/src-jvm/com/bot4s/telegram/clients/SttpClient.scala +++ b/core/src/com/bot4s/telegram/clients/SttpClient.scala @@ -7,7 +7,6 @@ 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 scala.concurrent.duration._ @@ -20,6 +19,7 @@ import sttp.client4.{ Request => SttpRequest } /** * Sttp HTTP client. + * Supports browsers via sttp's FetchBackend. * * @param token Bot token */ @@ -50,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))