Source: base_telegram_bot/base_telegram_bot.js

/**
 * @module base_telegram_bot
 */
'use strict';

import EventEmitter from 'events';
import { Worker } from 'worker_threads';
import path from 'path';
import got from 'got';

import { jt, getCallerName } from 'vvlad1973-utils';
import { ValidationError } from 'vvlad1973-error-definitions';
import SimpleLogger from 'vvlad1973-simple-logger';

import { createFormData } from '../../helpers/utils.js';

import {
  ParseModes,
  UpdateTypes,
  MessageTypes,
  TelegramBotApiMethods,
  StatusCodes,
  TelegramBotApiMethodsWithMedia,
  MediaProperties,
  PollTypes,
  ServiceMessages,
} from '../enums.js';
import { CallbackQuery } from '../callback_query.js';
import { Message } from '../message.js';

const
  telegramBotApiUrl = 'https://api.telegram.org/bot',
  telegramBotFileUrl = 'https://api.telegram.org/file/bot',
  instance = got.extend({
    handlers: [
      (options, next) => {
        Error.captureStackTrace(options.context);
        return next(options);
      },
    ],
    hooks: {
      beforeError: [
        (error) => {
          error.source = error.options.context.stack.split('\n');
          return error;
        },
      ],
    },
  });

/**
 * @class Implements Interface to Telegram Bot API
 * 
 * @description This object provide access to methods to communicate with 
 *     the Telegram
 * @readonly 
 * @property {integer} id - Read only. Unique identifier for this user or bot. This number
 *     may have more than 32 significant bits and some programming languages 
 *     may have difficulty/silent defects in interpreting it. But it has at 
 *     most 52 significant bits, so a 64-bit integer or double-precision float 
 *     type are safe for storing this identifier
 * @readonly
 * @property {string} firstName - Read only. User's or bot's first name
 * @readonly
 * @property {string} [username] - Read only. User's or bot's username
 * @readonly
 * @property {string} [lastName] - Read only. User's or bot's last name
 * @readonly
 * @property {string} [languageCode] - Read only. ETF language tag of the user's language
 * @readonly
 * @property {boolean} [canJoinGroups] - Read only. True, if the bot can be invited to 
 *     groups
 * @readonly
 * @property {boolean} [canReadAllGroupMessages] - Read only. True, if privacy mode is 
 *     disabled for the bot
 * @readonly
 * @property {boolean} [supportsInlineQueries] - Read only. True, if the bot supports 
 *     inline queries
 * @property {module:enums.ParseModes} [defaultParseMode] - Default value mode 
 *     for parsing messages wchich send by the bot
 * @property {Object} [logger] - Object implemented multilevel logging routines. If not 
 *     specified, will use default logger
 * @readonly
 * @property {Worker} sender - Read only. The bot's sender which will send data to 
 *     Telegram by using sending queue of messages
 * @property {boolean} [sendByQueue] - If True the bot will send data to 
 *     Telegram by using sending queue of messages
 * @readonly
 * @property {integer} lastMessageId - Read only. ID of the last sent message
 * @property {integer} [maxConnections] - The maximum allowed number of 
 *     simultaneous HTTPS connections to the webhook for update delivery, 1-100.
 *     Defaults to 40. Use lower values to limit the load on your bot's server, 
 *     and higher values to increase your bot's throughput
 * @property {string} [secretToken] - A secret token to be sent in a header 
 *     “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 
 *     characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. The 
 *     header is useful to ensure that the request comes from a webhook set 
 *     by you
 * @property {Object} [certificate] - Upload your public key certificate so
 *     that the root certificate in use can be checked. See our self-signed 
 *     guide for details
 * @property {string} [certificate.file] - Filename of the certificate
 * @property {Array.<string>} [allowedUpdates] - A JSON-serialized list of the 
 *     update types you want your bot to receive. For example, specify 
 *     [“message”, “edited_channel_post”, “callback_query”] to only receive 
 *     updates of these types. See Update for a complete list of available 
 *     update types. Specify an empty list to receive all update types except 
 *     chat_member (default). If not specified, the previous setting will be 
 *     used. 
 *     Please note that this parameter doesn't affect updates created before 
 *     the call to the setWebhook, so unwanted updates may be received for a 
 *     short period of time
 * @property {boolean} [dropPendingUpdates] - Pass True to drop all pending
 *     updates
 * @property {string} [ipAddress] - The fixed IP address which will be used 
 *     to send webhook requests instead of the IP address resolved through DNS
 */
export class BaseTelegramBot extends EventEmitter {

  #token;
  #secretToken
  #defaultParseMode;
  #senderPath;
  #id;
  #username;
  #firstName;
  #lastName;
  #languageCode;
  #canJoinGroups;
  #canReadAllGroupMessages;
  #supportsInlineQueries;

  webAppUrl;
  lastMessageId;
  totalSent = 0;
  totalReceived = 0;
  queueSize = 0;

  maxConnections;
  ipAddress;
  allowedUpdates;
  dropPendingUpdates;
  certificate;

  /**
   * @param {string} token - Each bot is given a unique authentication token
   *     when it is created. The token looks something like 
   *     123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
   * @param {Object} [options] - some optional parameters to pass to
   * @param {string} [options.url] - HTTPS URL to send updates to. Use an 
   *    empty string to remove webhook integration
   * @param {string} [options.secretToken] - A secret token to be sent in a 
   *     header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 
   *     1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. 
   *     The header is useful to ensure that the request comes from a webhook 
   *     set by you
   * @param {integer} [options.maxConnections] - The maximum allowed number of 
   *     simultaneous HTTPS connections to the webhook for update delivery, 
   *     1-100. Defaults to 40. Use lower values to limit the load on your 
   *     bot's server, and higher values to increase your bot's throughput
   * @param {boolean} [options.sendByQueue] - If True the bot will send data to 
   *     Telegram by using sending queue of messages
   * @param {Object} [options.certificate] - Upload your public key certificate so
   *     that the root certificate in use can be checked. See our self-signed 
   *     guide for details
   * @param {string} options.certificate.file - Frilename of the certificate
   * @param {Array.<string>} [options.allowedUpdates] - A JSON-serialized list of the 
   *     update types you want your bot to receive. For example, specify 
   *     [“message”, “edited_channel_post”, “callback_query”] to only receive 
   *     updates of these types. See Update for a complete list of available 
   *     update types. Specify an empty list to receive all update types except 
   *     chat_member (default). If not specified, the previous setting will be 
   *     used. 
   *     Please note that this parameter doesn't affect updates created before 
   *     the call to the setWebhook, so unwanted updates may be received for a 
   *     short period of time
   * @param {boolean} [options.dropPendingUpdates] - Pass True to drop all pending
   *     updates
   * @param {string} [options.ipAddress] - The fixed IP address which will be used 
   *     to send webhook requests instead of the IP address resolved through DNS
   * @param {module:enums.ParseModes} [defaultParseMode] - Default value mode 
   *     for parsing messages wchich send by the bot
   * @param {Object} [options.sender] - Params for the bot's sender which will send 
   *     data to Telegram by using sending queue of messages
   * @param {integer} [options.sender.capacityLimit] - Limit of amount of messages 
   *     to send during one interval of time. Default value is 30
   * @param {integer} [options.sender.capacityInterval] - Duration of one time 
   *     interval in milliseconds for message flow control. Default value is 1000
   * @param {integer} [options.sender.queueSize] - Limit of size of message queue. 
   *     Default value is 10 000
   * @param {Object} [options.sender.logger] - Object implemented multilevel logging 
   *     routines. If not specified, will use default logger
   * @returns {module:base_telegram_bot.BaseTelegramBot} Instance of the 
   *     BaseTelegramBot class
   */
  constructor(
    token,
    options = {}
  ) {
    super();

    this.#token = token;
    this.#secretToken = options.secretToken;
    this.ipAddress = options.ipAddress;
    this.allowedUpdates = options.allowedUpdates;
    this.url = options.url ?? '';
    this.dropPendingUpdates = options.dropPendingUpdates;
    this.certificate = options.certificate;
    this.sendByQueue = (typeof options.sendByQueue === 'undefined') ?
      true :
      options.sendByQueue;

    this.maxConnections = options.maxConnections ?? 40;

    this.logger = options.logger ?? new SimpleLogger();

    Object.defineProperties(this, {
      id: {
        get: () => {
          return this.#id;
        },
      },

      username: {
        get: () => {
          return this.#username;
        },
      },

      firstName: {
        get: () => {
          return this.#firstName;
        },
      },

      lastName: {
        get: () => {
          return this.#lastName;
        },
      },

      languageCode: {
        get: () => {
          return this.#languageCode;
        },
      },

      canJoinGroups: {
        get: () => {
          return this.#canJoinGroups;
        },
      },

      canReadAllGroupMessages: {
        get: () => {
          return this.#canReadAllGroupMessages;
        },
      },

      supportsInlineQueries: {
        get: () => {
          return this.#supportsInlineQueries;
        },
      },

      defaultParseMode: {
        get: () => {
          return this.#defaultParseMode;
        },

        set: (x) => {
          if (Object.values(ParseModes).includes(x)) {
            this.#defaultParseMode = x;
          } else {
            this.#defaultParseMode = ParseModes.NONE;
          }
        },
      },

      apiUrl: {
        get: () => {
          return `${telegramBotApiUrl}${this.#token}/`;
        },
      },

      fileUrl: {
        get: () => {
          return `${telegramBotFileUrl}${this.#token}/`;
        },
      },

      webhookUrl: {
        get: () => {
          let result;
          if (this.url)
            result = `${this.url}/${this.#token}`;
          return result;
        },

        set: (x) => {
          this.url = x;
        },
      },
    });

    this.defaultParseMode = options.defaultParseMode ?? ParseModes.NONE;

    if (process.env.TESTMODE)
      this.#senderPath = './classes/base_telegram_bot/sender.js';
    else
      this.#senderPath = path.resolve(
        'node_modules/vvlad1973-telegram-framework/classes/base_telegram_bot/sender.js'
      );

    this.sender = new Worker(this.#senderPath, {
      workerData: {
        url: this.apiUrl,
        capacity: {
          limit: options.sender?.capacityLimit ?? 30,
          interval: options.sender?.capacityInterval ?? 1000
        },
        queue: {
          size: options.sender?.queueSize ?? 10000
        }
      },
    });

    this.sender.logger = options.sender?.logger ?? this.logger;

    this.sender.on('message', (data) => {
      if (data?.error)
        this.sender.logger.error(
          data.error,
          `[TelegramBot.Sender] ${data.error.message}`
        );
      else if (data?.debug)
        this.sender.logger.debug(data.debug, `[TelegramBot.Sender]`);
      else if (data?.warn) this.sender.logger.warn(data.warn, `[TelegramBot.Sender]`);
      else if (data?.info) this.sender.logger.info(data.info, `[TelegramBot.Sender]`);
      else if (data?.trace)
        this.sender.logger.trace(data.trace, `[TelegramBot.Sender]`);
      else {
        if (
          typeof data.result === 'object' &&
          typeof data.request?.id === 'string'
        ) {
          if (data?.result?.ok && data?.response?.result?.message_id)
            this.lastMessageId = data?.response?.result?.message_id;

          this.emit(data.request.id, data);
        } else {
          throw new TypeError(
            `Unexpected content of Sender response: ${jt(data)}`
          );
        }
      }
    });
  }

  #emitIfExists(type, contents, params, chatId, user, data) {
    let userId = String(user?.id);

    this.eventNames().forEach((item) => {
      let filter;

      try {
        filter = JSON.parse(item);
      } catch (error) { }

      if (typeof filter === 'object') {
        let fIsDefined =
          typeof filter.type !== 'undefined' ||
          typeof filter.contents !== 'undefined' ||
          typeof filter.params !== 'undefined' ||
          typeof filter.chat !== 'undefined' ||
          typeof filter.user !== 'undefined';

        if (
          fIsDefined &&
          (typeof filter.type === 'undefined' ||
            (typeof filter.type &&
              typeof type !== 'undefined' &&
              type.match(filter.type))) &&
          (typeof filter.contents === 'undefined' ||
            (typeof filter.contents &&
              typeof contents !== 'undefined' &&
              contents.match(filter.contents))) &&
          (typeof filter.params === 'undefined' ||
            (typeof filter.params &&
              typeof params !== 'undefined' &&
              params.match(filter.params))) &&
          (typeof filter.chat === 'undefined' ||
            (typeof filter.chat &&
              typeof chatId !== 'undefined' &&
              chatId.match(filter.chat))) &&
          (typeof filter.user === 'undefined' ||
            (typeof filter.user &&
              typeof userId !== 'undefined' &&
              userId.match(filter.user)))
        ) {
          this.emit(item, type, contents, params, chatId, user, data);
        }
      }
    });
  }

  #getNextId() {
    this.totalSent++;
    return `${Date.now()}-${this.totalSent}`;
  }

  #logEntry(functionName) {
    functionName = functionName ?? getCallerName();
    this.logger.trace(`Function ${functionName}() execution...`);
  }

  #logExit(functionName) {
    functionName = functionName ?? getCallerName();
    this.logger.trace(`Function ${functionName}() complete`);
  }

  #getEventName(event) {
    let result;

    if (typeof event === 'object') {
      if (
        typeof event.type !== 'undefined' ||
        typeof event.contents !== 'undefined' ||
        typeof event.params !== 'undefined' ||
        typeof event.chat !== 'undefined' ||
        typeof event.user != 'undefined'
      ) {
        result = JSON.stringify(event);
      } else {
        throw new ValidationError('Filter is incorrect');
      }
    } else {
      result = event;
    }

    return result;
  }

  on(event, listener) {
    let eventName = this.#getEventName(event);

    super.on(eventName, listener);
  }

  once(event, listener) {
    let eventName = this.#getEventName(event);

    super.once(eventName, listener);
  }

  init() {
    this.#logEntry();

    return new Promise((resolve, reject) => {
      this.getMe()
        .then(
          (response) =>
            new Promise((resolve, reject) => {
              this.logger.trace(
                response.response?.result,
                `Bot initialisation data:`
              );

              this.#id = response.response?.result?.id;
              this.#firstName = response.response?.result?.first_name;
              this.#lastName = response.response?.result?.last_name;
              this.#username = response.response?.result?.username;
              this.#languageCode = response.response?.result?.language_code;
              this.#canJoinGroups = response.response?.result?.can_join_groups;
              this.#canReadAllGroupMessages =
                response.response?.result?.can_read_all_group_messages;
              this.#supportsInlineQueries =
                response.response?.result?.supports_inline_queries;

              this.getWebhookInfo()
                .then((response) => {
                  this.logger.trace(response, "Got bot's webhook info:");

                  return resolve(response);
                })
                .catch((error) => reject(error));
            })
        )
        .then(
          (response) =>
            new Promise((resolve, reject) => {
              if (this.webhookUrl !== response.response.result.url)
                this.setWebhook()
                  .then((response) => {
                    this.logger.info(response, "Bot's webhook was reset");
                    return resolve();
                  })
                  .catch((error) => {
                    return reject(error);
                  });
              else return resolve(response);
            })
        )
        .then((response) => {
          this.#logExit('init');

          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  processUpdate(data) {
    this.#logEntry();
    this.logger.debug(data, `Process update. Contents:`);

    return new Promise((resolve, reject) => {
      let object,
        parts,
        bot,
        command,
        type,
        contents =
          data?.message?.text ??
          data?.message?.caption ??
          data?.edited_message?.text ??
          data?.edited_message?.caption ??
          data?.channel_post?.text ??
          data?.channel_post?.caption ??
          data?.edited_channel_post?.text ??
          data?.edited_channel_post?.caption ??
          data?.callback_query?.data ??
          data?.inline_query?.query ??
          data?.chosenInlineResult?.result_id ??
          data?.poll?.question ??
          data?.poll_answer?.option_ids,
        chatId =
          data?.message?.chat?.id ??
          data?.edited_message?.chat?.id ??
          data?.channel_post?.chat?.id ??
          data?.edited_channel_post?.chat?.id ??
          data?.callback_query?.message?.chat?.id ??
          data?.my_chat_member?.chat?.id ??
          data?.chat_join_request?.chat?.id ??
          data?.chat_member?.chat?.id,
        user =
          data?.message?.from ??
          data?.edited_message?.from ??
          data?.channel_post?.from ??
          data?.edited_channel_post?.from ??
          data?.callback_query?.from ??
          data?.inline_query?.from ??
          data?.chosen_inline_result?.from ??
          data?.shipping_query?.from ??
          data?.pre_checkout_query?.from ??
          data?.inline_query?.from ??
          data?.poll_answer?.user ??
          data?.my_chat_member?.from ??
          data?.chat_join_request?.from ??
          data?.chat_member?.from,
        message = data?.message ? new Message(data?.message, this) : undefined,
        editedMessage = data?.edited_message
          ? new Message(data?.message, this)
          : undefined,
        channelPost = data?.channel_post
          ? new Message(data?.message, this)
          : undefined,
        editedСhannelPost = data?.edited_channel_post
          ? new Message(data?.message, this)
          : undefined,
        callbackQuery = data?.callback_query
          ? new CallbackQuery(data?.callback_query, this)
          : undefined,
        inlineQuery = data?.inline_query,
        chosenInlineResult = data?.chosen_inline_result,
        shippingQuery = data?.shipping_query,
        preCheckoutQuery = data?.pre_checkout_query,
        poll = data?.poll,
        pollAnswer = data?.poll_answer,
        myChatMember = data?.my_chat_member,
        chatMember = data?.chat_member,
        chatJoinRequest = data?.chat_join_request,
        params =
          message?.fileId ??
          data?.callback_query?.id ??
          data?.inline_query?.id ??
          data?.chosenInlineResult?.query ??
          data?.poll?.options ??
          data?.chat_join_request?.invite_link ??
          data?.poll_answer?.poll_id;

      if (message) {
        object = message;
        type = UpdateTypes.MESSAGE;

        if (typeof contents === 'string')
          parts = contents.match(/^[\\/]([^@\s]+)(@([\S]+))?( (.*))?$/);

        if (parts) {
          command = parts[1];
          params = parts[5] ? parts[5] : '';

          bot = parts[3] ? parts[3] : '';
        }

        type = message.type;
      } else if (callbackQuery) {
        object = callbackQuery;
        type = UpdateTypes.CALLBACK_QUERY;
      } else if (editedMessage) {
        object = editedMessage;
        type = UpdateTypes.EDITED_MESSAGE;
      } else if (channelPost) {
        object = channelPost;
        type = UpdateTypes.CHANNEL_POST;
      } else if (editedСhannelPost) {
        object = editedСhannelPost;
        type = UpdateTypes.EDITED_CHANNEL_POST;
      } else if (inlineQuery) {
        object = inlineQuery;
        type = UpdateTypes.INLINE_QUERY;
      } else if (chosenInlineResult) {
        object = chosenInlineResult;
        type = UpdateTypes.CHOSEN_INLINE_RESULT;
      } else if (preCheckoutQuery) {
        object = preCheckoutQuery;
        type = UpdateTypes.PRE_CHECKOUT_QUERY;
      } else if (poll) {
        object = poll;
        type = UpdateTypes.POLL;
      } else if (pollAnswer) {
        object = pollAnswer;
        type = UpdateTypes.POLL_ANSWER;
      } else if (myChatMember) {
        object = myChatMember;
        type = UpdateTypes.MY_CHAT_MEMBER;
      } else if (chatMember) {
        object = chatMember;
        type = UpdateTypes.CHAT_MEMBER;
      } else if (chatJoinRequest) {
        object = chatJoinRequest;
        type = UpdateTypes.CHAT_JOIN_REQUEST;
      } else if (shippingQuery) {
        object = shippingQuery;
        type = UpdateTypes.SHIPPING_QUERY;
      }

      if (object) {
        this.totalReceived++;

        if (chatId === user?.id)
          this.#emitIfExists(type, contents, params, 'private', user, object);
        else this.#emitIfExists(type, contents, params, chatId, user, object);

        if (command) {
          if (bot === '' || bot === this.username) {
            this.emit(
              MessageTypes.COMMAND,
              command,
              params,
              chatId,
              user,
              object
            );
            this.emit(command, contents, params, chatId, user, object);

            if (chatId === user?.id) {
              this.emit(
                `${MessageTypes.COMMAND}@private`,
                command,
                params,
                chatId,
                user,
                object
              );
              this.emit(
                `${command}@private`,
                contents,
                params,
                chatId,
                user,
                object
              );
            }
          }

          type = 'command';
          contents = command;
        }

        if (ServiceMessages.includes(type))
          this.emit('service_message', contents, params, chatId, user, object);

        this.emit('*', type, contents, params, chatId, user, object);

        if (chatId === user?.id)
          this.emit('*@private', type, contents, params, chatId, user, object);
      }

      this.#logExit('processUpdate');

      return resolve();
    });
  }

  #sendByQueue(data) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let id = this.#getNextId();

      this.sender.postMessage({ id: id, data: data });

      this.logger.debug(data, `[queueId=${id}] Data was enqueued:`);

      this.once(id, (data) => {
        if (data.result.ok) {
          this.logger.debug(data, `[queueId=${id}] Data was sent:`);

          if (data.queue_size) this.queueSize = data.queue_size;

          this.#logExit('#sendByQueue');
          return resolve(data);
        } else {
          return reject(data);
        }
      });
    });
  }

  #callApi(data) {
    this.#logEntry();

    let self = this;

    return new Promise(async (resolve, reject) => {
      let url = this.apiUrl,
        result,
        _options = {},
        response;

      if (data.multipart) {
        let form = createFormData(data.payload);

        _options = {
          headers: form.getHeaders(),
          body: form,
        };
      } else _options.json = data.payload;

      _options.responseType = 'json';

      instance
        .post(url, _options)
        .then((result) => {
          result = {
            result: {
              ok: true,
              code: StatusCodes.OK,
            },
            response: result.body,
            request: data,
          };

          self.logger.debug(result, `Data was sent:`);

          self.#logExit('callApi');
          return resolve(result);
        })
        .catch((error) => {
          let code;

          switch (error.code) {
            case 'ENOTFOUND': {
              code = StatusCodes.NOT_FOUND;
              break;
            }
            default:
              code = StatusCodes.BAD_REQUEST;
          }

          result = {
            result: {
              ok: false,
              code: code,
              message: error.message,
            },
            response: error.response?.body,
            request: data,
          };

          return reject(result);
        });
    });
  }

  #baseTelegramMethod(required, ...optional) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let data,
        hasMedia = false,
        result,
        obj = Object.assign(required, ...optional);

      if (obj._options) {
        obj = Object.assign(obj, obj._options);
        delete obj._options;
      }

      if (obj[MediaProperties.MEDIA]) {
        if (Array.isArray(obj[MediaProperties.MEDIA]))
          for (let item of obj[MediaProperties.MEDIA])
            if (item.media.file || item.thumbnail?.file) hasMedia = true;
      } else if (
        Object.values(TelegramBotApiMethodsWithMedia).includes(obj.method)
      ) {
        for (let item of Object.values(MediaProperties)) {
          if (Object.keys(obj).includes(item) && obj[item].file) {
            hasMedia = true;
          }
        }
      }
      data = {
        multipart: hasMedia,
        payload: obj,
      };

      try {
        if (this.sendByQueue) {
          result = await this.#sendByQueue(data);
        } else {
          result = await this.#callApi(data);
        }

        this.totalSent++;

        this.#logExit('#baseTelegramMethod');

        return resolve(result);
      } catch (error) {
        return reject(error);
      }
    });
  }

  getMe() {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let data = {
        method: TelegramBotApiMethods.GET_ME,
      };

      this.#baseTelegramMethod(data)
        .then((response) => {
          this.#logExit('getMe');
          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  getWebhookInfo() {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let data = {
        method: TelegramBotApiMethods.GET_WEBHOOK_INFO,
      };

      this.#baseTelegramMethod(data)
        .then((response) => {
          this.#logExit('getWebhookInfo');
          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  deleteWebhook(dropPendingUpdates = true) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let data = {
        method: TelegramBotApiMethods.DELETE_WEBHOOK,
      };

      if (!dropPendingUpdates) data['drop_pending_updates'] = false;

      this.#baseTelegramMethod(data)
        .then((response) => {
          this.#logExit('deleteWebhook');
          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  /**
   * Use this method to specify a URL and receive incoming updates via an 
   * outgoing webhook. Whenever there is an update for the bot, we will send 
   * an HTTPS POST request to the specified URL, containing a JSON-serialized 
   * Update. In case of an unsuccessful request, we will give up after a 
   * reasonable amount of attempts. Returns True on success. 
   * If you'd like to make sure that the webhook was set by you, you can 
   * specify secret data in the parameter secret_token. If specified, the 
   * request will contain a header “X-Telegram-Bot-Api-Secret-Token” with the 
   * secret token as content
   * @param {string} [url] - HTTPS URL to send updates to. Use an 
   *    empty string to remove webhook integration. Default value is empty 
   *    string
   * @param {Object} [options] - some optional parameters to pass to
   * @param {string} [options.secretToken] - A secret token to be sent in a 
   *     header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 
   *     1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. 
   *     The header is useful to ensure that the request comes from a webhook 
   *     set by you
   * @param {integer} [options.maxConnections] - The maximum allowed number of 
   *     simultaneous HTTPS connections to the webhook for update delivery, 
   *     1-100. Defaults to 40. Use lower values to limit the load on your 
   *     bot's server, and higher values to increase your bot's throughput
   * @param {boolean} [options.sendByQueue] - If True the bot will send data to 
   *     Telegram by using sending queue of messages
   * @param {Object} [options.certificate] - Upload your public key certificate so
   *     that the root certificate in use can be checked. See our self-signed 
   *     guide for details
   * @param {string} options.certificate.file - Frilename of the certificate
   * @param {Array.<string>} [options.allowedUpdates] - A JSON-serialized list of the 
   *     update types you want your bot to receive. For example, specify 
   *     [“message”, “edited_channel_post”, “callback_query”] to only receive 
   *     updates of these types. See Update for a complete list of available 
   *     update types. Specify an empty list to receive all update types except 
   *     chat_member (default). If not specified, the previous setting will be 
   *     used. 
   *     Please note that this parameter doesn't affect updates created before 
   *     the call to the setWebhook, so unwanted updates may be received for a 
   *     short period of time
   * @param {boolean} [options.dropPendingUpdates] - Pass True to drop all pending
   *     updates
   * @param {string} [options.ipAddress] - The fixed IP address which will be used 
   *     to send webhook requests instead of the IP address resolved through DNS
   * @returns {Object} Response with method executing result 
   */
  setWebhook(url, options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (typeof url === 'string')
        this.webhookUrl = url
      else
        url = this.webhookUrl;

      if (typeof this.webhookUrl === 'undefined')
        this.webhookUrl = '';

      if (options.maxConnections)
        this.maxConnections = options.maxConnections;

      if (options.secretToken)
        this.#secretToken = options.secretToken;

      if (options.ipAddress)
        this.ipAddress = options.ipAddress;

      if (options.allowedUpdates)
        this.allowedUpdates = options.allowedUpdates;

      if (options.dropPendingUpdates)
        this.dropPendingUpdates = options.dropPendingUpdates;

      if (options.certificate)
        this.certificate = options.certificate;

      let data = {
        method: TelegramBotApiMethods.SET_WEBHOOK,
        url: this.webhookUrl,
        max_connections: this.maxConnections,
        secret_token: this.#secretToken,
        ip_address: this.ipAddress,
        allowed_updates: this.allowedUpdates,
        drop_pending_updates: this.dropPendingUpdates,
        certificate: this.certificate
      };

      this.#baseTelegramMethod(data)
        .then((response) => {
          this.#logExit('setWebhook');
          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  sendMessage(chatId, text, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && text) {
        let data = {
          method: TelegramBotApiMethods.SEND_MESSAGE,
          chat_id: chatId,
          text: text,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        return this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendMessage');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('ChatId and text are required'));
    });
  }

  sendPhoto(chatId, photo, caption, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && photo) {
        let data = {
          method: TelegramBotApiMethods.SEND_PHOTO,
          chat_id: chatId,
          photo: photo,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendPhoto');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and photo are required'));
    });
  }

  sendMediaGroup(chatId, media, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && media) {
        let data = {
          method: TelegramBotApiMethods.SEND_MEDIA_GROUP,
          chat_id: chatId,
          media: media,
        };

        for (let mediaItem of media)
          mediaItem['parse_mode'] =
            mediaItem['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendMediaGroup');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError('ChatId and media group are required')
        );
    });
  }

  forwardMessage(chatId, fromChatId, messageId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && fromChatId && messageId) {
        let data = {
          method: TelegramBotApiMethods.FORWARD_MESSAGE,
          chat_id: chatId,
          from_chat_id: fromChatId,
          message_id: messageId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('forwardMessage');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError('ChatId, fromChatId and messageId are required')
        );
    });
  }

  copyMessage(chatId, fromChatId, messageId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && fromChatId && messageId) {
        let data = {
          method: TelegramBotApiMethods.COPY_MESSAGE,
          chat_id: chatId,
          from_chat_id: fromChatId,
          message_id: messageId,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('copyMessage');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError('ChatId, fromChatId and messageId are required')
        );
    });
  }

  deleteMessage(chatId, messageId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && messageId) {
        let data = {
          method: TelegramBotApiMethods.DELETE_MESSAGE,
          chat_id: chatId,
          message_id: messageId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('deleteMessage');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and messageId are required'));
    });
  }

  pinChatMessage(chatId, messageId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && messageId) {
        let data = {
          method: TelegramBotApiMethods.PIN_CHAT_MESSAGE,
          chat_id: chatId,
          message_id: messageId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('pinChatMessage');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return Promise.reject(
          new ValidationError('ChatId and messageId are required')
        );
    });
  }

  unpinChatMessage(chatId, messageId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && messageId) {
        let data = {
          method: TelegramBotApiMethods.UNPIN_CHAT_MESSAGE,
          chat_id: chatId,
          message_id: messageId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('unpinChatMessage');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and messageId are required'));
    });
  }

  unpinAllChatMessages(chatId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId) {
        let data = {
          method: TelegramBotApiMethods.UNPIN_ALL_CHAT_MESSAGES,
          chat_id: chatId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('unpinAllChatMessages');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('ChatId is required'));
    });
  }

  answerCallbackQuery(callbackQueryId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (callbackQueryId) {
        let data = {
          method: TelegramBotApiMethods.ANSWER_CALLBACK_QUERY,
          callback_query_id: callbackQueryId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('answerCallbackQuery');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('callbackQueryId is required'));
    });
  }

  sendDice(chatId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId) {
        let data = {
          method: TelegramBotApiMethods.SEND_DICE,
          chat_id: chatId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendDice');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return Promise.reject(new ValidationError('ChatId is required'));
    });
  }

  sendChatAction(chatId, action) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && action) {
        let data = {
          method: 'sendChatAction',
          chat_id: chatId,
          action: action,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('sendChatAction');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return Promise.reject(
          new ValidationError('ChatId and action are required')
        );
    });
  }

  getMyCommands(_options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let data = {
        method: TelegramBotApiMethods.GET_MY_COMMANDS,
      };

      this.#baseTelegramMethod(data, _options)
        .then((response) => {
          this.#logExit('getMyCommands');
          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  deleteMyCommands(_options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      let data = {
        method: TelegramBotApiMethods.DELETE_MY_COMMANDS,
      };

      this.#baseTelegramMethod(data, _options)
        .then((response) => {
          this.#logExit('deleteMyCommands');
          return resolve(response);
        })
        .catch((error) => reject(error));
    });
  }

  setMyCommands(commands, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (commands) {
        let data = {
          method: TelegramBotApiMethods.SET_MY_COMMANDS,
          commands: JSON.stringify(commands),
        };

        _options['scope'] = jt(_options['scope']);

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('setMyCommands');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('Commands are required'));
    });
  }

  editMessageText(text, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (
        text &&
        (_options.inline_message_id ||
          (_options.chat_id && _options.message_id))
      ) {
        let data = {
          method: TelegramBotApiMethods.EDIT_MESSAGE_TEXT,
          text: text,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('editMessageText');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError(
            'Text and inlineMessageId or chatId with ' +
            'messageId must be defined'
          )
        );
    });
  }

  editMessageCaption(caption, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (
        _options.inline_message_id ||
        (_options.chat_id && _options.message_id)
      ) {
        let data = {
          method: TelegramBotApiMethods.EDIT_MESSAGE_CAPTION,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('editMessageCaption');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError(
            'inlineMessageId or chatId with messageId must be defined'
          )
        );
    });
  }

  editMessageReplyMarkup(_options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (
        _options.inline_message_id ||
        (_options.chat_id && _options.message_id)
      ) {
        let data = {
          method: TelegramBotApiMethods.EDIT_MESSAGE_REPLY_MARKUP,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('editMessageReplyMarkup');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError(
            'inlineMessageId or chatId with messageId must be defined'
          )
        );
    });
  }

  editMessageMedia(media, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (
        _options.inline_message_id ||
        (_options.chat_id && _options.message_id)
      ) {
        media.parse_mode = media.parse_mode ?? this.defaultParseMode;

        let data = {
          method: TelegramBotApiMethods.EDIT_MESSAGE_MEDIA,
          media: jt(media),
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('editMessageMedia');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError(
            'inlineMessageId or chatId with messageId must be defined'
          )
        );
    });
  }

  sendVideo(chatId, video, caption, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && video) {
        let data = {
          method: TelegramBotApiMethods.SEND_VIDEO,
          chat_id: chatId,
          video: video,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendVideo');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and video are required'));
    });
  }

  sendContact(chatId, phoneNumber, firstName, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && phoneNumber && firstName) {
        let data = {
          method: TelegramBotApiMethods.SEND_CONTACT,
          chat_id: chatId,
          phone_number: phoneNumber,
          first_name: firstName,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendContact');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError('ChatId, phoneNumber and firstName are required')
        );
    });
  }

  sendAudio(chatId, audio, caption, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && audio) {
        let data = {
          method: TelegramBotApiMethods.SEND_AUDIO,
          chat_id: chatId,
          audio: audio,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendAudio');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and audio are required'));
    });
  }

  sendVoice(chatId, voice, caption, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && voice) {
        let data = {
          method: TelegramBotApiMethods.SEND_VOICE,
          chat_id: chatId,
          voice: voice,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendVoice');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and voice are required'));
    });
  }

  sendDocument(chatId, document, caption = '', _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && document) {
        let data = {
          method: TelegramBotApiMethods.SEND_DOCUMENT,
          chat_id: chatId,
          document: document,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendDocument');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and document are required'));
    });
  }

  sendAnimation(chatId, animation, caption = '', _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && animation) {
        let data = {
          method: TelegramBotApiMethods.SEND_ANIMATION,
          chat_id: chatId,
          animation: animation,
          caption: caption,
        };

        _options['parse_mode'] =
          _options['parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendAnimation');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and animation are required'));
    });
  }

  getChat(chatId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId) {
        let data = {
          method: TelegramBotApiMethods.GET_CHAT,
          chat_id: chatId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('getChat');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('ChatId is required'));
    });
  }

  getChatAdministrators(chatId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId) {
        let data = {
          method: TelegramBotApiMethods.GET_CHAT_ADMINISTRATORS,
          chat_id: chatId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('getChatAdministrators');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('ChatId is required'));
    });
  }

  getChatMemberCount(chatId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId) {
        let data = {
          method: TelegramBotApiMethods.GET_CHAT_MEMBER_COUNT,
          chat_id: chatId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('getChatMemberCount');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('ChatId is required'));
    });
  }

  getChatMember(chatId, userId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && userId) {
        let data = {
          method: TelegramBotApiMethods.GET_CHAT_MEMBER,
          chat_id: chatId,
          user_id: userId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('getChatMember');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and userId are required'));
    });
  }

  getFile(fileId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (fileId) {
        let data = {
          method: TelegramBotApiMethods.GET_FILE,
          file_id: fileId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {

            if (response?.response?.result?.file_path)
              response.response.result.file_url =
                `${this.fileUrl}/${response.response.result.file_path}`;

            this.#logExit('getFile');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else return reject(new ValidationError('FileId is required'));
    });
  }

  sendVideoNote(chatId, videoNote, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && videoNote) {
        let data = {
          method: TelegramBotApiMethods.SEND_VIDEO_NOTE,
          chat_id: chatId,
          video_note: videoNote,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendVideoNote');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and VideoNote are required'));
    });
  }

  sendPoll(chatId, question, pollOptions, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (
        chatId &&
        question &&
        pollOptions &&
        ((_options.type == PollTypes.QUIZ && _options.correct_option_id) ||
          _options.type !== PollTypes.QUIZ)
      ) {
        let data = {
          method: TelegramBotApiMethods.SEND_POLL,
          chat_id: chatId,
          question: question,
          options: jt(pollOptions),
        };

        _options['explanation_parse_mode'] =
          _options['explanation_parse_mode'] ?? this.defaultParseMode;

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendPoll');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(
          new ValidationError(
            'ChatId, question and options are required. ' +
            'If the pool type is quiz, correct_option_id is required too'
          )
        );
    });
  }

  stopPoll(chatId, messageId) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && messageId) {
        let data = {
          method: TelegramBotApiMethods.STOP_POLL,
          chat_id: chatId,
          message_id: messageId,
        };

        this.#baseTelegramMethod(data)
          .then((response) => {
            this.#logExit('stopPoll');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and messageId are required'));
    });
  }

  sendSticker(chatId, sticker, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && sticker) {
        let data = {
          method: TelegramBotApiMethods.SEND_STICKER,
          chat_id: chatId,
          sticker: sticker,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('sendSticker');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and Sticker are required'));
    });
  }

  banChatMember(chatId, userId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && userId) {
        let data = {
          method: TelegramBotApiMethods.BAN_CHAT_MEMBER,
          chat_id: chatId,
          user_id: userId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('banChatMember');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and userId are required'));
    });
  }

  unbanChatMember(chatId, userId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && userId) {
        let data = {
          method: TelegramBotApiMethods.UNBAN_CHAT_MEMBER,
          chat_id: chatId,
          user_id: userId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('unbanChatMember');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and userId are required'));
    });
  }

  restrictChatMember(chatId, userId, permissions, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && userId && permissions) {
        let data = {
          method: TelegramBotApiMethods.RESTRICT_CHAT_MEMBER,
          chat_id: chatId,
          user_id: userId,
          permissions: permissions,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('restrictChatMember');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and userId and permissions are required'));
    });
  }

  promoteChatMember(chatId, userId, _options = {}) {
    this.#logEntry();

    return new Promise(async (resolve, reject) => {
      if (chatId && userId) {
        let data = {
          method: TelegramBotApiMethods.PROMOTE_CHAT_MEMBER,
          chat_id: chatId,
          user_id: userId,
        };

        this.#baseTelegramMethod(data, _options)
          .then((response) => {
            this.#logExit('promoteChatMember');
            return resolve(response);
          })
          .catch((error) => reject(error));
      } else
        return reject(new ValidationError('ChatId and userId are required'));
    });
  }
}