limber-api-node-telegram
Version:
Module for creating Telegram bots.
654 lines (569 loc) • 17.1 kB
JavaScript
'use strict'
const InlineKeyboardButton = require('../models/InlineKeyboardButton')
const InlineKeyboardMarkup = require('../models/InlineKeyboardMarkup')
const ReplyKeyboardMarkup = require('../models/ReplyKeyboardMarkup')
const KeyboardButton = require('../models/KeyboardButton')
class Scope {
/**
*
* @param {Update} update
* @param {TelegramApi} api
* @param {BaseScopeExtension[]} extensions
* @param {Function[]} waitingRequests
* @param {Object} waitingCallbackQueries
* @param {BaseLogger} logger
* @param {Function} processUpdate
* @param {TelegramSessionStorage} sessionStorage
* @param {Function} waitForUpdate
* @param {Function} waitForCallback
*/
constructor(
update,
api,
extensions,
waitingRequests,
waitingCallbackQueries,
logger,
sessionStorage,
waitForUpdate,
waitForCallback
) {
this._api = api
this._update = update
/**
*
* @type {BaseScopeExtension[]}
* @private
*/
this._extensions = extensions
this._waitingRequests = waitingRequests
this._waitingCallbackQueries = waitingCallbackQueries
this._isEditedMessage = update.editedMessage ? true : false
this._message = update.message || update.editedMessage
this._chatId = this._message.chat.id
this._userId = this._message.from.id
this._fromGroupChat = !(this._userId === this._chatId)
this._logger = logger
this._sessionStorage = sessionStorage
this._waitForUpdate = waitForUpdate
this._waitForCallback = waitForCallback
this._extensions.forEach(extension => {
const extensionInstance = new extension(this)
this[extensionInstance.name] = extensionInstance.process
})
}
/**
* @returns {TelegramSessionStorage}
*/
get sessionStorage() {
return this._sessionStorage
}
/**
* @returns {BaseStorage}
*/
get storage() {
return this._sessionStorage
}
/**
*
* @returns {Update}
*/
get update() {
return this._update
}
/**
*
* @returns {Message}
*/
get message() {
return this._message
}
/**
*
* @returns {number}
*/
get chatId() {
return this._chatId
}
/**
*
* @returns {number}
*/
get userId() {
return this._userId
}
/**
*
* @returns {boolean}
*/
get idFromGroupChat() {
return this._fromGroupChat
}
/**
*
* @returns {TelegramApi}
*/
get api() {
return this._api
}
/**
* @param {string} key
* @returns {Promise.<*>}
*/
getUserSession(key) {
return this._sessionStorage.getUserSession(this.userId, key)
}
/**
* @param {string} key
* @param {*} value
* @returns {Promise}
*/
setUserSession(key, value) {
return this._sessionStorage.setUserSession(this.userId, key, value)
}
/**
* @param {string} key
* @returns {Promise.<*>}
*/
getChatSession(key) {
return this._sessionStorage.getChatSession(this.chatId, key)
}
/**
* @param {string} key
* @param {*} value
* @returns {Promise}
*/
setChatSession(key, value) {
return this._sessionStorage.setChatSession(this.chatId, key, value)
}
/**
*
* @returns {BaseLogger}
*/
get logger() {
return this._logger
}
/**
*
* @returns {boolean}
*/
get isEditedMessage() {
return this._isEditedMessage
}
/**
* After calling this the next update
* from current user will be passed to promise
*
* @returns {Promise<Scope>}
*/
get waitForRequest() {
return new Promise(resolve => {
this._waitingRequests[this.chatId] = resolve
this._waitForUpdate(this.chatId)
})
}
/**
* @callback waitForCallbackQueryCallback
* @param {CallbackQuery} query
*/
/**
* If you send some inline keyboard after that you can call this method,
* pass to it string callback data or array of string or your InlineKeyboardMarkup
* and then when user press button CallbackQuery will be passed to callback
*
* @param {string|string[]|InlineKeyboardMarkup} data
* @param {waitForCallbackQueryCallback} callback
*/
waitForCallbackQuery(data, callback) {
if (typeof data === 'string') {
this._waitForCallback(data)
this._waitingCallbackQueries[data] = callback
}
if (Array.isArray(data)) {
data.forEach(item => {
this._waitForCallback(item)
this._waitingCallbackQueries[item] = callback
})
}
if (data instanceof InlineKeyboardMarkup) {
data.inlineKeyboard.forEach(line => {
line.forEach(key => {
this._waitForCallback(key.callbackData)
this._waitingCallbackQueries[key.callbackData] = callback
})
})
}
}
/**
*
* @param {Object} menuData
*/
runMenu(menuData) {
const startMessage = menuData.message
const ignoredKeys = [
'message',
'layout',
'options',
'resizeKeyboard',
'oneTimeKeyboard',
'anyMatch'
]
const keys = Object.keys(menuData)
let keyboard = []
if (menuData.layout) {
let lineIndex = 0
keys.forEach(key => {
if (ignoredKeys.indexOf(key) === -1) {
if (!keyboard[lineIndex])
keyboard[lineIndex] = []
keyboard[lineIndex].push(new KeyboardButton(key))
if (typeof menuData.layout === 'number') {
if (keyboard[lineIndex].length === menuData.layout) {
lineIndex++
}
} else {
if (keyboard[lineIndex].length === menuData.layout[lineIndex]) {
lineIndex++
}
}
}
})
} else {
keys.forEach(key => {
if (ignoredKeys.indexOf(key) === -1) {
keyboard.push([new KeyboardButton(key)])
}
})
}
const resizeKeyboard = (menuData.resizeKeyboard && menuData.resizeKeyboard === true)
const oneTimeKeyboard = (menuData.oneTimeKeyboard && menuData.oneTimeKeyboard === true)
let replyMarkup = new ReplyKeyboardMarkup(keyboard, resizeKeyboard, oneTimeKeyboard)
let options = {
reply_markup: JSON.stringify(replyMarkup)
}
if (menuData.options) options = Object.assign(options, menuData.options)
this.sendMessage(startMessage, options)
this.waitForRequest
.then($ => {
if (keys.indexOf($.message.text) > -1 &&
ignoredKeys.indexOf($.message.text) === -1) {
if (typeof menuData[$.message.text] === 'object') {
$.runMenu(menuData[$.message.text])
} else {
menuData[$.message.text]($)
}
} else if (menuData.anyMatch) {
menuData.anyMatch($)
} else {
$.runMenu(menuData)
}
})
}
/**
*
* @callback runFormCallback
* @param {Object} response
*/
/**
*
* @param {Object} formData
* @param {runFormCallback} callback
*/
runForm(formData, callback) {
let i = 0
const run = () => {
const key = keys[i]
this.sendMessage(formData[key].q, {
disable_web_page_preview: true,
reply_markup: formData[key].keyboard ? JSON.stringify({
one_time_keyboard: true,
resize_keyboard: formData[key].resize_keyboard || false,
keyboard: formData[key].keyboard
}) : ''
})
this.waitForRequest
.then($ => {
formData[key].validator($.message, (valid, value) => {
if (valid === true) {
result[key] = value
i++
if (i === Object.keys(formData).length) {
try {
callback(result)
}
catch (e) {
this.logger.error({ 'error in user callback:': e })
}
return
}
run()
} else {
this.sendMessage(formData[key].error, {
disable_web_page_preview: true
})
.then(() => {
run()
})
}
})
})
}
let result = {}
const keys = Object.keys(formData)
run()
}
/**
*
* @param {Object} menuData
*/
runInlineMenu(menuData, prevMessage) {
const method = menuData.method
const params = menuData.params || []
const layout = menuData.layout
const menu = menuData.menu
let keyboard = []
let callbackData = []
if (!layout) {
keyboard = menu.map(item => {
callbackData.push(Math.random().toString(36).substring(7))
return [new InlineKeyboardButton(
item.text,
item.url,
callbackData[callbackData.length - 1]
)]
})
}
else {
let line = 0
menu.forEach(item => {
if (!keyboard[line]) keyboard[line] = []
callbackData.push(Math.random().toString(36).substring(7))
keyboard[line].push(new InlineKeyboardButton(
item.text,
item.url,
callbackData[callbackData.length - 1]
))
let goToNextLine = Array.isArray(layout) ? keyboard[line].length ===
layout[line] : keyboard[line].length === layout
if (goToNextLine)
line++
})
}
if (typeof params[params.length - 1] === 'object') {
params[params.length - 1] = Object.assign(params[params.length - 1], {
reply_markup: JSON.stringify(new InlineKeyboardMarkup(keyboard))
})
}
else {
params.push({
reply_markup: JSON.stringify(new InlineKeyboardMarkup(keyboard))
})
}
var prepareCallback = (response) => {
callbackData.forEach((data, index) => {
this.waitForCallbackQuery(data, (query) => {
if (menu[index].callback)
try {
menu[index].callback(query, response)
}
catch (e) {
this.logger.error({ 'error in user callback:': e })
}
else {
this.runInlineMenu(menu[index], response)
}
})
})
}
if (!prevMessage) {
this[method].apply(this, params)
.then(response => {
prepareCallback(response)
})
}
else {
params[0].chat_id = prevMessage.chat.id
params[0].message_id = prevMessage.messageId
this.api.editMessageText(menuData.message, params[0])
.then(response => {
prepareCallback(response)
})
}
}
//api methods starts here
/**
*
* @param {string} text
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendMessage(text, options) {
return this._api.sendMessage(this.chatId, text, options)
}
/**
*
* @param {number} fromChatId
* @param {number} messageId
* @param {Object} [options]
* @returns {Promise<Message>}
*/
forwardMessage(fromChatId, messageId, options) {
return this._api.forwardMessage(this.chatId, fromChatId, messageId, options)
}
/**
*
* @param {InputFile|Object} photo
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendPhoto(photo, options) {
return this._api.sendPhoto(this.chatId, photo, options)
}
/**
*
* @param {InputFile|Object} audio
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendAudio(audio, options) {
return this._api.sendAudio(this.chatId, audio, options)
}
/**
*
* @param {InputFile|Object} document
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendDocument(document, options) {
return this._api.sendDocument(this.chatId, document, options)
}
/**
*
* @param {InputFile|Object} sticker
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendSticker(sticker, options) {
return this._api.sendSticker(this.chatId, sticker, options)
}
/**
*
* @param {InputFile|Object} video
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendVideo(video, options) {
return this._api.sendVideo(this.chatId, video, options)
}
/**
*
* @param {InputFile|Object} voice
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendVoice(voice, options) {
return this._api.sendVoice(this.chatId, voice, options)
}
/**
*
* @param {number} latitude
* @param {number} longitude
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendLocation(latitude, longitude, options) {
return this._api.sendLocation(this.chatId, latitude, longitude, options)
}
/**
*
* @param {number} latitude
* @param {number} longitude
* @param {string} title
* @param {string}address
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendVenue(latitude, longitude, title, address, options) {
return this._api.sendVenue(this.chatId, latitude, longitude, title, address, options)
}
/**
*
* @param {string} phoneNumber
* @param {string} firstName
* @param {Object} [options]
* @returns {Promise<Message>}
*/
sendContact(phoneNumber, firstName, options) {
return this._api.sendContact(this.chatId, phoneNumber, firstName, options)
}
/**
*
* @param {string} action
* @returns {Promise<Object>}
*/
sendChatAction(action) {
return this._api.sendChatAction(this.chatId, action)
}
/**
*
* @param {number} offset
* @param {number} limit
* @returns {Promise<UserProfilePhotos>}
*/
getUserProfilePhotos(offset, limit) {
return this._api.getUserProfilePhotos(userId, offset, limit)
}
/**
*
* @param {number} userId
* @returns {Promise.<boolean>}
*/
kickChatMember(userId) {
return this._api.kickChatMember(this.chatId, userId)
}
/**
*
* @returns {Promise.<boolean>}
*/
leaveChat() {
return this._api.leaveChat(this.chatId)
}
/**
*
* @param {number} userId
* @returns {Promise.<boolean>}
*/
unbanChatMember(userId) {
return this._api.unbanChatMember(this.chatId, userId)
}
/**
*
* @returns {Promise<Chat>}
*/
getChat() {
return this._api.getChat(this.chatId)
}
/**
*
* @returns {Promise<ChatMember[]>}
*/
getChatAdministrators() {
return this._api.getChatAdministrators(this.chatId)
}
/**
*
* @returns {Promise<number>}
*/
getChatMembersCount() {
return this._api.getChatMembersCount(this.chatId)
}
/**
*
* @param {number} userId
* @returns {Promise.<ChatMember>}
*/
getChatMember(userId) {
return this._api.getChatMember(this.chatId, userId)
}
}
module.exports = Scope