scaladoc update, and added Update.sourceTime

This commit is contained in:
A.C.Sukazyo Eyre 2024-02-15 18:33:17 +08:00
parent 456273be96
commit 9cc8b49459
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
14 changed files with 237 additions and 34 deletions

View File

@ -8,7 +8,7 @@ object MornyConfiguration {
val MORNY_CODE_STORE = "https://github.com/Eyre-S/Coeur-Morny-Cono" val MORNY_CODE_STORE = "https://github.com/Eyre-S/Coeur-Morny-Cono"
val MORNY_COMMIT_PATH = "https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s" val MORNY_COMMIT_PATH = "https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s"
val VERSION = "2.0.0-alpha12" val VERSION = "2.0.0-alpha13"
val VERSION_DELTA: Option[String] = None val VERSION_DELTA: Option[String] = None
val CODENAME = "guanggu" val CODENAME = "guanggu"

View File

@ -3,3 +3,4 @@ addSbtPlugin("no.arktekk.sbt" % "aether-deploy" % "0.29.1")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")
addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1") addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5")
//addSbtPlugin("com.glngn" % "sbt-alldocs" % "0.3.0")

View File

@ -24,7 +24,7 @@ object MornyCoeur {
/** A tag that shows current [[MornyCoeur!]] is running under /** A tag that shows current [[MornyCoeur!]] is running under
* test mode. * test mode.
* *
* @see [[MornyCoeur.testRun]] for more introduction for test mode. * @see *Test Mode* in [[MornyCoeur]] for more introduction for test mode.
* @since 2.0.0 * @since 2.0.0
*/ */
object TestRun object TestRun
@ -88,7 +88,7 @@ object MornyCoeur {
* *
* ## Lifecycle * ## Lifecycle
* *
* todo... * todo
* *
* ## Test Mode * ## Test Mode
* *

View File

@ -4,7 +4,7 @@ trait EventListener () {
/** Determine if this event listener should be processed. /** Determine if this event listener should be processed.
* *
* Default implementation is it only be [[true]] when the event * Default implementation is it only be `true` when the event
* is not ok yet (when [[EventEnv.isEventOk]] is false). * is not ok yet (when [[EventEnv.isEventOk]] is false).
* *
* Notice that: You should not override this method to filter some * Notice that: You should not override this method to filter some
@ -13,7 +13,7 @@ trait EventListener () {
* method is just for event low-level controls. * method is just for event low-level controls.
* *
* @param env The [[EventEnv event variable]]. * @param env The [[EventEnv event variable]].
* @return [[true]] if this event listener should run; [[false]] * @return `true` if this event listener should run; `false`
* if it should not run. * if it should not run.
*/ */
def executeFilter (using env: EventEnv): Boolean = def executeFilter (using env: EventEnv): Boolean =

View File

@ -11,7 +11,7 @@ import com.pengrad.telegrambot.UpdatesListener
import scala.collection.mutable import scala.collection.mutable
import scala.language.postfixOps import scala.language.postfixOps
/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]]. /** Contains a [[scala.collection.mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]].
* *
* Implemented [[process]] in [[UpdatesListener]] so it can directly used in [[com.pengrad.telegrambot.TelegramBot.setupListener]]. * Implemented [[process]] in [[UpdatesListener]] so it can directly used in [[com.pengrad.telegrambot.TelegramBot.setupListener]].
* *

View File

@ -2,16 +2,26 @@ package cc.sukazyo.cono.morny.core.bot.event
import cc.sukazyo.cono.morny.core.MornyCoeur import cc.sukazyo.cono.morny.core.MornyCoeur
import cc.sukazyo.cono.morny.core.bot.api.{EventEnv, EventListener} import cc.sukazyo.cono.morny.core.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Update.sourceTime
import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis
class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener { class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener {
private def checkOutdated (timestamp: Int)(using event: EventEnv): Unit = override def executeFilter (using env: EventEnv): Boolean =
if coeur.config.eventIgnoreOutdated && (timestamp < (coeur.coeurStartTimestamp/1000)) then if (
event.setEventCanceled (env.update.message != null) ||
(env.update.editedMessage != null) ||
(env.update.channelPost != null) ||
(env.update.editedChannelPost != null)
)
true
else false
override def onMessage (using event: EventEnv): Unit = checkOutdated(event.update.message.date) override def on (using event: EventEnv): Unit =
override def onEditedMessage (using event: EventEnv): Unit = checkOutdated(event.update.editedMessage.date) event.update.sourceTime match
override def onChannelPost (using event: EventEnv): Unit = checkOutdated(event.update.channelPost.date) case Some(timestamp) =>
override def onEditedChannelPost (using event: EventEnv): Unit = checkOutdated(event.update.editedChannelPost.date) if coeur.config.eventIgnoreOutdated && (EpochMillis.fromEpochSeconds(timestamp) < coeur.coeurStartTimestamp) then
event.setEventCanceled
case None =>
} }

View File

@ -4,7 +4,6 @@ import cats.effect.IO
import cc.sukazyo.cono.morny.core.http.api.HttpService4Api import cc.sukazyo.cono.morny.core.http.api.HttpService4Api
import cc.sukazyo.cono.morny.data.TelegramImages import cc.sukazyo.cono.morny.data.TelegramImages
import org.http4s.{HttpRoutes, MediaType} import org.http4s.{HttpRoutes, MediaType}
import org.http4s.dsl.impl./
import org.http4s.dsl.io.* import org.http4s.dsl.io.*
import org.http4s.headers.`Content-Type` import org.http4s.headers.`Content-Type`

View File

@ -11,7 +11,7 @@ import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtm
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import cc.sukazyo.cono.morny.util.EpochDateTime.DurationMillis import cc.sukazyo.cono.morny.util.EpochDateTime.DurationMillis
import cc.sukazyo.cono.morny.util.schedule.CronTask import cc.sukazyo.cono.morny.util.schedule.CronTask
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Update.{extractSourceChat, extractSourceUser} import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Update.{sourceChat, sourceUser}
import cc.sukazyo.cono.morny.util.CommonEncrypt.hashId import cc.sukazyo.cono.morny.util.CommonEncrypt.hashId
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
import com.cronutils.builder.CronBuilder import com.cronutils.builder.CronBuilder
@ -206,9 +206,9 @@ class MornyReport (using coeur: MornyCoeur) {
override def atEventPost (using event: EventEnv): Unit = { override def atEventPost (using event: EventEnv): Unit = {
import event.* import event.*
eventTotal += 1 eventTotal += 1
event.update.extractSourceChat match event.update.sourceChat match
case None => case None =>
event.update.extractSourceUser match event.update.sourceUser match
case None => case None =>
case Some(user) => case Some(user) =>
event_from_user_action << hashId(user.id).toHex event_from_user_action << hashId(user.id).toHex

View File

@ -11,18 +11,18 @@ import io.circe.{DecodingFailure, ParsingFailure}
* *
* @see [[https://github.com/FixTweet/FixTweet/wiki/Status-Fetch-API]] * @see [[https://github.com/FixTweet/FixTweet/wiki/Status-Fetch-API]]
* *
* @param code Status code, normally be [[200]], but can be 401 * @param code Status code, normally be `200`, but can be 401
* or [[404]] or [[500]] due to different reasons. * or `404` or `500` due to different reasons.
* *
* Related to [[message]] * Related to [[message]]
* @param message Status message. * @param message Status message.
* *
* - When [[code]] is [[200]], it should be `OK` * - When [[code]] is `200`, it should be `OK`
* - When [[code]] is [[401]], it should be `PRIVATE_TWEET`, * - When [[code]] is `401`, it should be `PRIVATE_TWEET`,
* while in practice, it seems PRIVATE_TWEET will * while in practice, it seems PRIVATE_TWEET will
* just return [[404]]. * just return `404`.
* - When [[code]] is [[404]], it should be `NOT_FOUND` * - When [[code]] is `404`, it should be `NOT_FOUND`
* - When [[code]] is [[500]], it should be `API_FILE` * - When [[code]] is `500`, it should be `API_FILE`
* @param tweet [[FXTweet]] content. * @param tweet [[FXTweet]] content.
* @since 1.3.0 * @since 1.3.0
* @version 2023.11.21 * @version 2023.11.21
@ -86,7 +86,7 @@ object FXApi {
* @throws DecodingFailure When cannot decode the API response to a [[FXApi]] * @throws DecodingFailure When cannot decode the API response to a [[FXApi]]
* object. It might be some wrong with the [[FXApi]] * object. It might be some wrong with the [[FXApi]]
* model, or the remote API spec changes. * model, or the remote API spec changes.
* @return a [[FXApi]] response object, with [[200]] or any other response code. * @return a [[FXApi]] response object, with `200` or any other response code.
*/ */
@throws[SttpClientException|ParsingFailure|DecodingFailure] @throws[SttpClientException|ParsingFailure|DecodingFailure]
def status (screen_name: Option[String], id: String, translate_to: Option[String] = None): FXApi = def status (screen_name: Option[String], id: String, translate_to: Option[String] = None): FXApi =

View File

@ -12,8 +12,8 @@ class HackerEventHandler (using hacker: EventHacker)(using coeur: MornyCoeur) ex
override def on (using event: EventEnv): Unit = override def on (using event: EventEnv): Unit =
given update: Update = event.update given update: Update = event.update
if hacker.trigger( if hacker.trigger(
update.extractSourceChat.map[Long](_.id).getOrElse(0), update.sourceChat.map[Long](_.id).getOrElse(0),
update.extractSourceUser.map[Long](_.id).getOrElse(0) update.sourceUser.map[Long](_.id).getOrElse(0)
) then ) then
event.setEventOk event.setEventOk

View File

@ -33,7 +33,7 @@ object CommonFormat {
* @param utcOffset the hour offset of the time zone, the time-zone controls * @param utcOffset the hour offset of the time zone, the time-zone controls
* which local time describe will use. * which local time describe will use.
* *
* for example, timestamp [[0]] describes 1970-1-1 00:00:00 in * for example, timestamp `0` describes 1970-1-1 00:00:00 in
* UTC+0, so, use the `timestamp` `0` and `utfOffset` `0` will * UTC+0, so, use the `timestamp` `0` and `utfOffset` `0` will
* returns `"1970-1-1 00:00:00:000"`; however, at the same time, * returns `"1970-1-1 00:00:00:000"`; however, at the same time,
* in UTC+8, the local time is 1970-1-1 08:00:00:000, so use * in UTC+8, the local time is 1970-1-1 08:00:00:000, so use

View File

@ -43,7 +43,7 @@ trait RoutineTask extends Task {
* *
* @param previousRoutineScheduledTimeMillis The previous task routine's * @param previousRoutineScheduledTimeMillis The previous task routine's
* scheduled time. * scheduled time.
* @return The next task routine's scheduled time, or [[null]] means end * @return The next task routine's scheduled time, or `null` means end
* of the task. * of the task.
*/ */
def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis|Null def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis|Null

View File

@ -151,7 +151,7 @@ class Scheduler {
schedule(task) schedule(task)
this this
/** Add one task to scheduler task queue. /** Add one task to scheduler task queue.
* @return [[true]] if the task is added. * @return `true` if the task is added.
*/ */
def schedule (task: Task): Boolean = def schedule (task: Task): Boolean =
taskList.synchronized: taskList.synchronized:
@ -175,7 +175,7 @@ class Scheduler {
* If the removal task is running, the method will wait for the current run * If the removal task is running, the method will wait for the current run
* complete (and current run post effect complete), then do remove. * complete (and current run post effect complete), then do remove.
* *
* @return [[true]] if the task is in task queue or is running, and have been * @return `true` if the task is in task queue or is running, and have been
* succeed removed from task queue. * succeed removed from task queue.
*/ */
def cancel (task: Task): Boolean = def cancel (task: Task): Boolean =

View File

@ -1,6 +1,7 @@
package cc.sukazyo.cono.morny.util.tgapi package cc.sukazyo.cono.morny.util.tgapi
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
import cc.sukazyo.cono.morny.util.EpochDateTime.{EpochMillis, EpochSeconds}
import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.TelegramBot
import com.pengrad.telegrambot.model.* import com.pengrad.telegrambot.model.*
import com.pengrad.telegrambot.request.{BaseRequest, GetChatMember} import com.pengrad.telegrambot.request.{BaseRequest, GetChatMember}
@ -12,8 +13,31 @@ object TelegramExtensions {
object Bot { extension (bot: TelegramBot) { object Bot { extension (bot: TelegramBot) {
/** Try sync execute a [[BaseRequest request]], and throws [[EventRuntimeException]]
* when it fails.
*
* It will use [[Telegram.execute]] to execute the request, and check if the response is failed.
*
* If the request returned a [[BaseResponse response]], the [[BaseResponse.isOk]] will be checked.
* if it is false, the method will ended with a [[EventRuntimeException.ActionFailed]], which
* message is param [[onError_message]](or [[BaseResponse.errorCode]] if it is empty).
*
* If the request failed in the client (that means [[TelegramBot.execute]] call failed with Exceptions),
* this method will ended with a [[EventRuntimeException.ClientFailed]].
*
* @param request The request needed to be run.
* @param onError_message The exception message that will be thrown when the [[request]]'s
* [[BaseResponse response]] is not ok([[BaseResponse.isOk isOk()]] == false)
* @tparam T Type of the request
* @tparam R Type of the response that request should returns.
* @throws EventRuntimeException Whenever the request's response is not ok, or the request does not
* return a response. See above for more info.
* @return The succeed response (which is returned by [[TelegramBot.execute]]) as is.
*
* @since 1.0.0
*/
@throws[EventRuntimeException] @throws[EventRuntimeException]
def exec [T <: BaseRequest[T, R], R <: BaseResponse] (request: T, onError_message: String = ""): R = { def exec [T <: BaseRequest[T, R], R <: BaseResponse] (request: BaseRequest[T, R], onError_message: String = ""): R = {
try { try {
val response = bot execute request val response = bot execute request
if response isOk then return response if response isOk then return response
@ -31,7 +55,41 @@ object TelegramExtensions {
object Update { extension (update: Update) { object Update { extension (update: Update) {
def extractSourceChat: Option[Chat] = /** Get the [[Chat]] that the update comes from.
*
* For the update types that is non-chat related, or the update types that framework does not
* supported yet, this method will return [[None]].
*
* ### Supported Update Types
*
* Belows are the supported update types that will return a valid [[Chat]].
*
* - [[Update.message]]: Chat that the message belongs.
* - [[Update.editedMessage]]: Chat that the edited message belongs.
* - [[Update.channelPost]]: Chat that the channel post message belongs.
* - [[Update.editedChannelPost]]: Chat that the edited channel post message belongs.
* - [[Update.callbackQuery]]: Chat that the callback query's source message (a callback query
* is triggered from a inline message button, that inline message is the source message) belongs.
* - [[Update.myChatMember]]: Chat that where member status of current bot changed there.
* - [[Update.chatMember]]: Chat that any one user's member status changed there.
* - [[Update.chatJoinRequest]]: Chat that the join request comes from.
*
* Belows are the known update types that is not chat related, so there's only a [[None]] returned.
*
* - [[Update.inlineQuery]]
* - [[Update.chosenInlineResult]]
* - [[Update.shippingQuery]]
* - [[Update.preCheckoutQuery]]
* - [[Update.poll]]
* - [[Update.pollAnswer]]
*
* Supported up to Telegram Bot API 6.2
*
* @return An [[Option]] either contains a [[Chat]] that the update comes from, or [[None]] if there's
* no user or update type is unsupported.
* @since 2.0.0
*/
def sourceChat: Option[Chat] =
if (update.message != null) Some(update.message.chat) if (update.message != null) Some(update.message.chat)
else if (update.editedMessage != null) Some(update.editedMessage.chat) else if (update.editedMessage != null) Some(update.editedMessage.chat)
else if (update.channelPost != null) Some(update.channelPost.chat) else if (update.channelPost != null) Some(update.channelPost.chat)
@ -48,7 +106,42 @@ object TelegramExtensions {
else if (update.chatJoinRequest != null) Some(update.chatJoinRequest.chat) else if (update.chatJoinRequest != null) Some(update.chatJoinRequest.chat)
else None else None
def extractSourceUser: Option[User] = /** Get the [[User]] that the update comes from.
*
* For the update types that is non-user related, or the update types that framework does not
* supported yet, this method will return [[None]].
*
* ### Supported Update Types
*
* Belows are the supported update types that will return a valid [[User]].
*
* - [[Update.message]]: User that sent the message.
* - [[Update.editedMessage]]: User that sent the message. Notice that is the user who ORIGINAL
* SENT the message but NOT EDITED the message due to the API limitation, while in current Bot
* API version (v7.0), editing other's message is not allowed so it should have no problem.
* - [[Update.inlineQuery]]: User that executing the inline query.
* - [[Update.chosenInlineResult]]: User that chosen the inline result of a inline query.
* - [[Update.callbackQuery]]: User that triggered the callback query.
* - [[Update.shippingQuery]]: User that sent the shipping query.
* - [[Update.preCheckoutQuery]]: User that sent the pre-checkout query.
* - [[Update.pollAnswer]]: User that answered a un-anonymous poll.
* - [[Update.myChatMember]]: Current bot that my member status changed.
* - [[Update.chatMember]]: User that their member status changed.
* - [[Update.chatJoinRequest]]: User that sent a join request.
*
* Belows are the known update types that is not user related, so there's only a [[None]] returned.
*
* - [[Update.channelPost]]
* - [[Update.editedChannelPost]]
* - [[Update.poll]] <small>(odd, but it is no sender in the latest Bot API 7.0)</small>
*
* Supported up to Telegram Bot API 6.2
*
* @return An [[Option]] either contains a [[User]] that the update comes from, or [[None]] if there's
* no user or update type is unsupported.
* @since 2.0.0
*/
def sourceUser: Option[User] =
if (update.message != null) Some(update.message.from) if (update.message != null) Some(update.message.from)
else if (update.editedMessage != null) Some(update.editedMessage.from) else if (update.editedMessage != null) Some(update.editedMessage.from)
else if (update.channelPost != null) None else if (update.channelPost != null) None
@ -65,13 +158,113 @@ object TelegramExtensions {
else if (update.chatJoinRequest != null) Some(update.chatJoinRequest.from) else if (update.chatJoinRequest != null) Some(update.chatJoinRequest.from)
else None else None
/** Date time of the update.
*
* This date-time is guessed from the update content -- it may not response the accurate or even real
* time when the update happens. Some update types contains no related time information, so it can only
* returns [[None]].
*
* Telegram uses UNIX seconds as the time unit, and Java Telegram Bot API (which this project
* dependents this) use [[Int]] to store it. so the returned time will be formatted to [[EpochSeconds]]
* which also uses UNIX seconds as the time unit, and stored in type [[Int]].
*
* ### Supported Update Types
*
* Belows are the supported update types that will return a valid time.
*
* - [[Update.message]]: Time that the message is sent. Should be fixed and accurate.
* - [[Update.editedMessage]]: Time that the message is edited. Can be changed when the message is
* edited again, but should be the fixed editing time in the specific update.
* - [[Update.channelPost]]: Time that the channel post message is sent. Should be fixed and accurate.
* - [[Update.editedChannelPost]]: Time that the channel post message is edited. Same with
* [[Update.editedMessage]]
* - [[Update.myChatMember]]: Time that the member status of current bot changes done.
* - [[Update.chatMember]]: Time that one user's member status changes done.
* - [[Update.chatJoinRequest]]: Time that the join request is sent.
*
* Belows have no any time information, so there's only a [[None]] returned.
*
* - [[Update.inlineQuery]]
* - [[Update.chosenInlineResult]]
* - [[Update.callbackQuery]]
* - [[Update.shippingQuery]]
* - [[Update.preCheckoutQuery]]
* - [[Update.poll]]
* - [[Update.pollAnswer]]
*
* Supported up to Telegram Bot API 6.2
*
* @return An [[Option]] either contains a [[EpochSeconds]] that may be the time when update happens,
* or [[None]] if there's unsupported.
* @since 2.0.0
*/
def sourceTime: Option[EpochSeconds] =
if (update.message != null) Some(update.message.date)
else if (update.editedMessage != null) Some(update.editedMessage.editDate)
else if (update.channelPost != null) Some(update.channelPost.date)
else if (update.editedChannelPost != null) Some(update.editedChannelPost.editDate)
else if (update.inlineQuery != null) None
else if (update.chosenInlineResult != null) None
else if (update.callbackQuery != null) None
else if (update.shippingQuery != null) None
else if (update.preCheckoutQuery != null) None
else if (update.poll != null) None
else if (update.pollAnswer != null) None
else if (update.myChatMember != null) Some(update.myChatMember.date)
else if (update.chatMember != null) Some(update.chatMember.date)
else if (update.chatJoinRequest != null) Some(update.chatJoinRequest.date)
else None
}} }}
object Chat { extension (chat: Chat) { object Chat { extension (chat: Chat) {
/** Check if a user is a member of this chat.
*
* It is equivalent to `memberHasPermission(user, ChatMember.Status.member)`.
*
* It checks if the user's member status is [[ChatMember.Status.member]], so if the member is in
* this chat but they are been [[ChatMember.Status.restricted]], it will be treated as *not a member*.
*
* It needs to execute a request getting chat member, so it requires a live [[TelegramBot]] instance,
* and the bot needs to be a member of this chat so that it can read the chat member.
*
* **Notice:** This method will execute a request to the Telegram Bot API, so it may be slow.
*
* @see [[memberHasPermission]]
*
* @param user The user that want to check if they are a member of this chat.
* @return [[true]] when the user is a member of this chat, [[false]] otherwise.
*
* @since 1.0.0
*/
def hasMember (user: User) (using TelegramBot): Boolean = def hasMember (user: User) (using TelegramBot): Boolean =
memberHasPermission(user, ChatMember.Status.member) memberHasPermission(user, ChatMember.Status.member)
/** Check if a user has a permission level in this chat.
*
* It checks if the user's member status is equal or greater than the required permission level.
*
* Due to API does not implemented a permission level system, so inside this method, there's a
* permission level table:
*
* | [[ChatMember.Status]] | Permission Level |
* |----------------------|-----------------:|
* | [[ChatMember.Status.creator]] | 10 |
* | [[ChatMember.Status.administrator]] | 3 |
* | [[ChatMember.Status.member]] | 1 |
* | [[ChatMember.Status.restricted]] | -1 |
* | [[ChatMember.Status.left]] | -3 |
* | [[ChatMember.Status.kicked]] | -5 |
*
* **Notice:** This method will execute a request to the Telegram Bot API, so it may be slow.
*
* @param user The user that wanted to check if they have the permission.
* @param permission The required permission level.
* @param bot A live [[TelegramBot]] that will be used to execute the getChatMember request. It
* should be a member of this chat so that can read the chat member for this method works.
* @return [[true]] if the user have the permission or higher permission, [[false]] otherwise.
*/
def memberHasPermission (user: User, permission: ChatMember.Status) (using bot: TelegramBot): Boolean = { def memberHasPermission (user: User, permission: ChatMember.Status) (using bot: TelegramBot): Boolean = {
//noinspection ScalaUnusedSymbol //noinspection ScalaUnusedSymbol