pax_global_header00006660000000000000000000000064134455026440014521gustar00rootroot0000000000000052 comment=030be73cbbb7f6096f668ceb2a4faa350c9e1dfb telegram-bot-api-1.3.3/000077500000000000000000000000001344550264400146565ustar00rootroot00000000000000telegram-bot-api-1.3.3/.gitignore000066400000000000000000000000631344550264400166450ustar00rootroot00000000000000node_modules *.log test-old .env package-lock.json telegram-bot-api-1.3.3/.travis.yml000066400000000000000000000000461344550264400167670ustar00rootroot00000000000000language: node_js node_js: - "node" telegram-bot-api-1.3.3/LICENSE000066400000000000000000000020701344550264400156620ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Max Stepanov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. telegram-bot-api-1.3.3/README.md000066400000000000000000000133451344550264400161430ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/mast/telegram-bot-api.svg?branch=master)](https://travis-ci.org/mast/telegram-bot-api) [![npm version](https://badge.fury.io/js/telegram-bot-api.svg)](https://badge.fury.io/js/telegram-bot-api) ## Introduction Node.js module for Telegram Bot API (https://core.telegram.org/bots/api). You can use it simply as an API if you want to implement logic by yourself, or you can enable retrieving of updates and get messages sent to your bot in a callback IMPORTANT: In version 1.0.0. promises are implemented in backward compatible way, i.e. old code (with callbacks) should also work with new version of the API ## Installation You can use npm package to get module installed ``` npm install telegram-bot-api ``` ## Example (simple API usage) ``` var telegram = require('telegram-bot-api'); var api = new telegram({ token: '' }); api.getMe() .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); }); ``` ## Supported methods For method parameters and description, please refer to official documentation https://core.telegram.org/bots/api This module supports following methods so far: ``` getMe sendMessage forwardMessage sendPhoto sendAudio sendVoice sendDocument sendSticker sendVideo sendLocation sendVenue sendContact sendChatAction getUserProfilePhotos getUpdates setWebhook getFile answerInlineQuery answerCallbackQuery editMessageText editMessageCaption editMessageReplyMarkup kickChatMember unbanChatMember exportChatInviteLink leaveChat getChat getChatAdministrators getChatMembersCount getChatMember ``` ## Retrieve messages sent to your bot You can force this API to retrieve messages sent to your Telegram Bot. API will emit *message* event as soon as some message is received by your bot. Please note, that you need explicitly configure this behaviour, as it is disabled by default. ``` var telegram = require('telegram-bot-api'); var api = new telegram({ token: '', updates: { enabled: true } }); api.on('message', function(message) { // Received text message console.log(message); }); api.on('inline.query', function(message) { // Received inline query console.log(message); }); api.on('inline.result', function(message) { // Received chosen inline result console.log(message); }); api.on('inline.callback.query', function(message) { // New incoming callback query console.log(message); }); api.on('edited.message', function(message) { // Message that was edited console.log(message); }); api.on('update', function(message) { // Generic update object // Subscribe on it in case if you want to handle all possible // event types in one callback console.log(message); }); ``` ## Example (send photo) ``` var telegram = require('telegram-bot-api'); var api = new telegram({ token: '', }); api.sendPhoto({ chat_id: , caption: 'This is my test image', // you can also send file_id here as string (as described in telegram bot api documentation) photo: '/path/to/file/test.jpg' }) .then(function(data) { console.log(util.inspect(data, false, null)); }); ``` ## Other examples Please refer to `/examples` folder of repository. ## API configuration You should pass configuration object to API constructor, which have following fields. | Param name | Mandatory? | Description | |---|---|---| | token | Mandatory | Telegram access token (received from BotFather) | | http_proxy | Optional | This object is optional. Use it in case if you want API to connect through proxy | | http_proxy.host | Mandatory | Proxy hostname | | http_proxy.port | Mandatory | Proxy port | | http_proxy.user | Optional | Username (authentication) | | http_proxy.password | Optional | Password (authentication) | | http_proxy.https | Optional | Pass `true` if you want `https` used as a protocol. Default `false` | | updates | Optional | Pass it to configure how API will handle incoming messages to your bot | | updates.enabled | Optional | `true` – API will listen for messages and provide you with callback. `false` – API will not listen for messages, care about it by yourself. Default `false` | | updates.get_interval | Optional | This number of milliseconds API will poll Telegram servers for messages. Default `1000` | | updates.pooling_timeout | Optional | This number of milliseconds API will keep connection with Telegram servers alive. Default `0` | Example of configuration object ``` { token: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', http_proxy: { host: 'proxy.example.com', port: 8080, username: 'mylogin', password: 'mypassword' }, updates: { enabled: true, get_interval: 2000 } } ``` ## License The MIT License (MIT) Copyright (c) 2015 Max Stepanov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. telegram-bot-api-1.3.3/examples/000077500000000000000000000000001344550264400164745ustar00rootroot00000000000000telegram-bot-api-1.3.3/examples/echo-bot.js000066400000000000000000000011321344550264400205270ustar00rootroot00000000000000var telegram = require('telegram-bot-api'); var api = new telegram({ token: '', updates: { enabled: true, get_interval: 1000 } }); api.on('message', function(message) { var chat_id = message.chat.id; // It'd be good to check received message type here // And react accordingly // We consider that only text messages can be received here api.sendMessage({ chat_id: message.chat.id, text: message.text ? message.text : 'This message doesn\'t contain text :(' }) .then(function(message) { console.log(message); }) .catch(function(err) { console.log(err); }); }); telegram-bot-api-1.3.3/examples/getme.js000066400000000000000000000003451344550264400201350ustar00rootroot00000000000000var telegram = require('telegram-bot-api'); var api = new telegram({ token: '' }); api.getMe() .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); }); telegram-bot-api-1.3.3/examples/init-inline-keyboard.js000066400000000000000000000023171344550264400230520ustar00rootroot00000000000000var telegram = require('telegram-bot-api'); var api = new telegram({ token: '', updates: { enabled: true } }); //Create your inline keyboard markup var inlineKeyboard = { inline_keyboard: [ [ { text: 'Row 1 Cell 1', callback_data: '1-1' }, { text: 'Row 1 Cell 2', callback_data: '1-2' } ], [ { text: 'Row 2', callback_data: '2' } ] ] }; /* | 1-1 | 1-2 | | 2 | */ api.sendMessage({ chat_id: , text: 'Click on buttons below', reply_markup: JSON.stringify(inlineKeyboard) }) .then(function(message) { console.log(message); }) .catch(function(err) { console.log(err); }); //When user click on button, 'CallbackQuery' Object will be catch by code below api.on('inline.callback.query', function(msg) { var data = msg.data; //Value from 'callback_data' field of clicked button console.log(data); //do stuff }); telegram-bot-api-1.3.3/examples/send-message.js000066400000000000000000000005151344550264400214060ustar00rootroot00000000000000var telegram = require('telegram-bot-api'); var util = require('util'); var api = new telegram({ token: '' }); api.sendMessage({ chat_id: , text: 'This is my kind message to you' }) .then(function(data) { console.log(util.inspect(data, false, null)); }) .catch(function(err) { console.log(err); }); telegram-bot-api-1.3.3/examples/send-photo.js000066400000000000000000000006501344550264400211130ustar00rootroot00000000000000var telegram = require('telegram-bot-api'); var api = new telegram({ token: '' }); api.sendPhoto({ chat_id: , caption: 'This is my test image', // you can also send file_id here as string (as described in telegram bot api documentation) photo: '/path/to/file/test.jpg' }) .then(function(data) { console.log(util.inspect(data, false, null)); }) .catch(function(err) { console.log(err); }); telegram-bot-api-1.3.3/lib/000077500000000000000000000000001344550264400154245ustar00rootroot00000000000000telegram-bot-api-1.3.3/lib/telegram-bot.js000066400000000000000000001767371344550264400203710ustar00rootroot00000000000000 var util = require('util'); var fs = require('fs'); var extend = require('extend'); var request = require('request-promise'); var EventEmitter = require('events').EventEmitter; var Promise = require('bluebird'); var express = require('express') var https = require('https'); var webapp = express() Promise.onPossiblyUnhandledRejection(function(error) { throw error; }) /** * API params * token access token for bot * baseUrl Base URL for telegram API * http_proxy proxy settings (optional) * host proxy host * port proxy port * user username for proxy * password password for proxy * https true/false * updates * enabled True if you want to receive updates from telegram (default false) * get_interval We will fetch updates from Telegram * each number of milliseconds (default 1000) * pooling_timeout We will wait for updates during this num of * milliseconds at each attempt before quit (default 0) * webhook * enabled True if you want to receive updates via webhook * (one one of updates.enabled or webhook.enabled could be true) */ var TelegramApi = function (params) { // Manage REST connection settings var proxy = null; if (params.http_proxy !== undefined) { if (params.http_proxy.https === true) { proxy = 'https://'; } else { proxy = 'http://'; } if (params.http_proxy.user !== undefined && params.http_proxy.password !== undefined) { proxy += params.http_proxy.user + ':' + params.http_proxy.password + '@'; } proxy += params.http_proxy.host + ':' + params.http_proxy.port; } // Create some global vars var self = this; var _updatesOffset = 0; var _rest = request.defaults({ proxy: proxy }); // Default settings var _settings = { updates: { enabled: false, get_interval: 1000, pooling_timeout: 0 }, webhook: { enabled: false, url: undefined, certificate: undefined, max_connections: 40, allowed_updates: [], host: '0.0.0.0', port: 8443 } }; // Extend default settings with passed params extend(true, _settings, params); // Warn use in case if he miss some important params if (!_settings.token) { console.error('[TelegramBot]: you should pass access token in params'); } // Validate params if (_settings.updates.enabled == true && _settings.webhook.enabled == true) { throw new Error('telegram updates and webhook are mutually exclusive') } // This is base url for API var _baseurl = _settings.baseUrl ? _settings.baseUrl : 'https://api.telegram.org' _baseurl += '/bot' + _settings.token + '/'; /** * INTERNAL FUNCTION * Parse response */ function commonResponseHandler(data) { return new Promise(function(resolve, reject) { if (data.ok === false) { // Request failed reject({ description: data.description !== undefined ? data.description : 'Not set by API', code: data.error_code !== undefined ? data.error_code : 'Not set by API' }); } else { resolve(data.result); } }); } /** * INTERNAL FUNCTION * Gets updates from telegram and starts loop */ function internalGetUpdates() { self.getUpdates({ offset: _updatesOffset, limit: 50, timeout: _settings.updates.pooling_timeout }) .then(function (data) { if (!data) { throw new Error("[TelegramBot]: Received data is empty."); } else { // We received some items, loop over them ('forEach' in data) && data.forEach(function (item) { // Account update_id as next offset // to avoid dublicated updates _updatesOffset = (item.update_id >= _updatesOffset ? item.update_id + 1 : _updatesOffset); // Update events self.emit('update', item); // Inline events if (item.callback_query) { self.emit('inline.callback.query', item.callback_query); } if (item.edited_message) { self.emit('edited.message', item.edited_message); } // On inline query is received if(item.inline_query) { self.emit('inline.query', item.inline_query); } // On inline result is chosen if(item.chosen_inline_result) { self.emit('inline.result', item.chosen_inline_result); } // Notify subscriber if (item.message) { self.emit('message', item.message); } }); } // Schedule follow up setTimeout(internalGetUpdates, _settings.updates.get_interval); }) .catch(function(err) { console.error('[TelegramBot]: Failed to get updates from Telegram servers'); console.error(err) setTimeout(internalGetUpdates, _settings.updates.get_interval); }); } /** * METHOD: getMe * PARAMS: * none * cb is opitonal, you may use promises */ this.getMe = function (cb) { return new Promise(function(resolve, reject) { _rest({ method: 'GET', json: true, uri: _baseurl + 'getMe' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: sendMessage * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * text Text of the message to be sent * disable_web_page_preview Disables link previews for links in this message * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object * for a custom reply keyboard, instructions to hide keyboard * or to force a reply from the user. * parse_mode Send Markdown, if you want Telegram apps to show bold, italic and * inline URLs in your bot's message */ this.sendMessage = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.text !== undefined) args.text = params.text; if (params.disable_web_page_preview !== undefined) args.disable_web_page_preview = params.disable_web_page_preview; if (params.reply_to_message_id !== undefined) args.reply_to_message_id = params.reply_to_message_id; if (params.reply_markup !== undefined) args.reply_markup = params.reply_markup; if (params.parse_mode !== undefined) args.parse_mode = params.parse_mode; return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendMessage' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: forwardMessage * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * from_chat_id Unique identifier for the chat where the original message was sent — User or GroupChat id * message_id Unique message identifier */ this.forwardMessage = function (params, cb) { return new Promise(function(resolve, reject) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.from_chat_id !== undefined) args.from_chat_id = params.from_chat_id; if (params.message_id !== undefined) args.message_id = params.message_id; _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'forwardMessage' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: sendPhoto * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * photo Photo to send. You can either pass a file_id as String to resend * a photo that is already on the Telegram servers, or upload * a new photo using multipart/form-data. * caption Photo caption (may also be used when resending photos by file_id) * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. */ this.sendPhoto = function (params, cb) { return new Promise(function(resolve, reject) { // Act different depending on value params.photo fs.exists(params.photo, function (exists) { var photo = null; if (exists) { // params.photo is path to file photo = fs.createReadStream(params.photo); } else { // params.photo is not a file, simply pass it to POST photo = params.photo; } var args = { chat_id: params.chat_id, photo: photo }; if (params.caption !== undefined) { args.caption = params.caption; } if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendPhoto' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: sendAudio * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * audio Audio file to send. You can either pass a file_id as String to resend * a audio that is already on the Telegram servers, or upload * a new audio using multipart/form-data. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. * duration Duration of the audio in seconds (optional) * performer Performer (optional) * title Track name (optional) */ this.sendAudio = function (params, cb) { return new Promise(function(resolve, reject) { // Act different depending on value params.audio fs.exists(params.audio, function (exists) { var audio = null; if (exists) { // params.audio is path to file audio = fs.createReadStream(params.audio); } else { // params.audio is not a file, simply pass it to POST audio = params.audio; } var args = { chat_id: params.chat_id, audio: audio }; if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } if (params.duration !== undefined) { args.duration = params.duration; } if (params.performer !== undefined) { args.performer = params.performer; } if (params.title !== undefined) { args.title = params.title; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendAudio' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: sendVoice * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * voice Audio file to send. You can either pass a file_id as String to resend * a audio that is already on the Telegram servers, or upload * a new audio using multipart/form-data. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. * duration Duration of the audio in seconds (optional) */ this.sendVoice = function (params, cb) { return new Promise(function(resolve, reject) { // Act different depending on value params.voice fs.exists(params.voice, function (exists) { var voice = null; if (exists) { // params.voice is path to file voice = fs.createReadStream(params.voice); } else { // params.voice is not a file, simply pass it to POST voice = params.voice; } var args = { chat_id: params.chat_id, voice: voice }; if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } if (params.duration !== undefined) { args.duration = params.duration; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendVoice' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: sendDocument * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * document File to send. You can either pass a file_id as String to resend * a file that is already on the Telegram servers, or upload * a new file using multipart/form-data. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. */ this.sendDocument = function (params, cb) { return new Promise(function(resolve, reject) { // Act different depending on value params.document fs.exists(params.document, function (exists) { var document = null; if (exists) { // params.document is path to file document = fs.createReadStream(params.document); } else { // params.document is not a file, simply pass it to POST document = params.document; } var args = { chat_id: params.chat_id, document: document }; if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendDocument' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: sendSticker * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * sticker Sticker to send. You can either pass a file_id as String to resend * a sticker that is already on the Telegram servers, or upload * a new sticker using multipart/form-data. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. */ this.sendSticker = function (params, cb) { return new Promise(function(resolve, reject) { // Act different depending on value params.sticker fs.exists(params.sticker, function (exists) { var sticker = null; if (exists) { // params.sticker is path to file sticker = fs.createReadStream(params.sticker); } else { // params.sticker is not a file, simply pass it to POST sticker = params.sticker; } var args = { chat_id: params.chat_id, sticker: sticker }; if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendSticker' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: sendVideo * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * video Video to send. You can either pass a file_id as String to resend * a video that is already on the Telegram servers, or upload * a new video using multipart/form-data. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. * duration Duration in seconds (optional) * caption Video caption (may also be used when resending videos by file_id), 0-200 characters */ this.sendVideo = function (params, cb) { return new Promise(function(resolve, reject) { // Act different depending on value params.video fs.exists(params.video, function (exists) { var video = null; if (exists) { // params.video is path to file video = fs.createReadStream(params.video); } else { // params.video is not a file, simply pass it to POST video = params.video; } var args = { chat_id: params.chat_id, video: video }; if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } if (params.duration !== undefined) { args.duration = params.duration; } if (params.caption !== undefined) { args.caption = params.caption; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendVideo' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: sendLocation * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * latitude Latitude of location * longitude Longitude of location * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. */ this.sendLocation = function (params, cb) { return new Promise(function(resolve, reject) { var args = { chat_id: params.chat_id, latitude: params.latitude, longitude: params.longitude }; if (params.reply_to_message_id !== undefined) { args.reply_to_message_id = params.reply_to_message_id; } if (params.reply_markup !== undefined) { args.reply_markup = params.reply_markup; } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendLocation' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: sendVenue * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * latitude Latitude of location * longitude Longitude of location * title Name of the venue * address Address of the venue * foursquare_id Foursquare identifier of the venue * disable_notification Sends the message silently. iOS users will not receive a notification, * Android users will receive a notification with no sound. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. */ this.sendVenue = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'sendVenue' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: sendContact * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * phone_number Contact's phone number * first_name Contact's first name * last_name Contact's last name * disable_notification Sends the message silently. iOS users will not receive a notification, * Android users will receive a notification with no sound. * reply_to_message_id If the message is a reply, ID of the original message * reply_markup Additional interface options. A JSON-serialized object for a custom * reply keyboard, instructions to hide keyboard or to force a reply from the user. */ this.sendContact = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'sendContact' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: kickChatMember * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) * user_id Unique identifier of the target user */ this.kickChatMember = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'kickChatMember' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: unbanChatMember * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) * user_id Unique identifier of the target user */ this.unbanChatMember = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'unbanChatMember' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: restrictChatMember * PARAMS: * See https://core.telegram.org/bots/api#restrictchatmember */ this.restrictChatMember = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'restrictChatMember' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: promoteChatMember * PARAMS: * See https://core.telegram.org/bots/api#promotechatmember */ this.promoteChatMember = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'promoteChatMember' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: leaveChat * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) */ this.leaveChat = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'leaveChat' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getChat * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) */ this.getChat = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'getChat' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getChatAdministrators * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) */ this.getChatAdministrators = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'getChatAdministrators' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getChatMembersCount * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) */ this.getChatMembersCount = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'getChatMembersCount' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getChatMember * PARAMS: * chat_id Unique identifier for the target group or * username of the target supergroup (in the format @supergroupusername) * user_id Unique identifier of the target user */ this.getChatMember = function (params, cb) { return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: params, uri: _baseurl + 'getChatMember' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: sendChatAction * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * action Type of action to broadcast. Choose one, depending on what the user * is about to receive: typing for text messages, upload_photo for photos, * record_video or upload_video for videos, record_audio or upload_audio * for audio files, upload_document for general files, find_location for location data */ this.sendChatAction = function (params, cb) { return new Promise(function(resolve, reject) { var args = { chat_id: params.chat_id, action: params.action }; _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendChatAction' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getUserProfilePhotos * PARAMS: * user_id Unique identifier of the target user * offset Sequential number of the first photo to be returned. By default, all photos are returned * limit Limits the number of photos to be retrieved. Values between 1—100 are accepted. Defaults to 100 */ this.getUserProfilePhotos = function (params, cb) { return new Promise(function(resolve, reject) { var args = {}; if (params.user_id !== undefined) args.user_id = params.user_id; if (params.offset !== undefined) args.offset = params.offset; if (params.limit !== undefined) args.limit = params.limit; _rest({ method: 'GET', json: true, formData: args, uri: _baseurl + 'getUserProfilePhotos' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getUpdates * PARAMS: * offset Identifier of the first update to be returned. Must be greater by one than the highest * among the identifiers of previously received updates. By default, updates starting with * the earliest unconfirmed update are returned. An update is considered confirmed as soon as * getUpdates is called with an offset higher than its update_id. * limit Limits the number of updates to be retrieved. Values between 1—100 are accepted. Defaults to 100 * timeout Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling */ this.getUpdates = function (params, cb) { return new Promise(function(resolve, reject) { var args = {}; if (params.timeout !== undefined) args.timeout = params.timeout; if (params.offset !== undefined) args.offset = params.offset; if (params.limit !== undefined) args.limit = params.limit; _rest({ method: 'GET', json: true, formData: args, uri: _baseurl + 'getUpdates' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: setWebhook * PARAMS: * url HTTPS url to send updates to. Use an empty string to remove webhook integration * certificate Filename of public key certificate (optional) */ this.setWebhook = function (params, cb) { return new Promise(function(resolve, reject) { var args = {}; if (params.url !== undefined) args.url = params.url; if (!params.certificate) { return _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'setWebhook' }) .then(commonResponseHandler) .then(resolve) .catch(reject); } // Check existance of certificate fs.exists(params.certificate, function (exists) { if (exists) { // params.video is path to file args.certificate = fs.createReadStream(params.certificate); } _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'setWebhook' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }); }).nodeify(cb); }; /** * METHOD: deleteWebhook * PARAMS: * NONE */ this.deleteWebhook = function (cb) { return new Promise(function(resolve, reject) { var args = {}; _rest({ method: 'POST', json: true, uri: _baseurl + 'deleteWebhook' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getWebhookInfo * PARAMS: * NONE */ this.getWebhookInfo = function (cb) { return new Promise(function(resolve, reject) { var args = {}; _rest({ method: 'POST', json: true, uri: _baseurl + 'getWebhookInfo' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: getFile * PARAMS: * file_id File identifier to get info about */ this.getFile = function (params, cb) { return new Promise(function(resolve, reject) { var args = {}; if (params.file_id !== undefined) args.file_id = params.file_id; _rest({ method: 'GET', json: true, formData: args, uri: _baseurl + 'getFile' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: answerInlineQuery * PARAMS: * inline_query_id Unique identifier for the answered query * results Array of results for the inline query (API will serialize it by itself) * cache_time The maximum amount of time in seconds that the result of the inline * query may be cached on the server. Defaults to 300. * is_personal Pass True, if results may be cached on the server side only for the user * that sent the query. By default, results may be returned to any user * who sends the same query * next_offset Pass the offset that a client should send in the next query with * the same text to receive more results. Pass an empty string if there are * no more results or if you don‘t support pagination. Offset length can’t exceed 64 bytes. */ this.answerInlineQuery = function (params, cb) { return new Promise(function(resolve, reject) { var args = {}; if (params.inline_query_id !== undefined) args.inline_query_id = params.inline_query_id; if (params.results !== undefined) args.results = JSON.stringify(params.results); if (params.cache_time !== undefined) args.cache_time = params.cache_time; if (params.is_personal !== undefined) args.is_personal = params.is_personal; if (params.next_offset !== undefined) args.next_offset = params.next_offset; if (params.switch_pm_text !== undefined) args.switch_pm_text = params.switch_pm_text; if (params.switch_pm_parameter !== undefined) args.switch_pm_parameter = params.switch_pm_parameter; _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'answerInlineQuery' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: answerCallbackQuery * PARAMS: * callback_query_id Unique identifier for the query to be answered * text Text of the notification. If not specified, nothing will be shown to the user * show_alert If true, an alert will be shown by the client instead of a notificaiton at the * top of the chat screen. Defaults to false. */ this.answerCallbackQuery = function (params, cb) { return new Promise(function (resolve, reject) { var args = {}; if (params.callback_query_id !== undefined) args.callback_query_id = params.callback_query_id; if (params.text !== undefined) args.text = params.text; if (params.show_alert !== undefined) args.show_alert = JSON.stringify(params.show_alert); if (params.url !== undefined) args.url = params.url; _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'answerCallbackQuery' }) .then(function (body) { return commonResponseHandler(body); }) .then(function (data) { resolve(data); }) .catch(function (err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: editMessageText * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * message_id Required if inline_message_id is not specified. Unique identifier of the sent message * inline_message_id Required if chat_id and message_id are not specified. Identifier of the inline message * text Text of the message to be sent * parse_mode Send Markdown, if you want Telegram apps to show bold, italic and * inline URLs in your bot's message * disable_web_page_preview Disables link previews for links in this message * reply_markup Additional interface options. A JSON-serialized object * for a custom reply keyboard, instructions to hide keyboard * or to force a reply from the user. */ this.editMessageText = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.message_id !== undefined) args.message_id = params.message_id; if (params.inline_message_id !== undefined) args.inline_message_id = params.inline_message_id; if (params.text !== undefined) args.text = params.text; if (params.parse_mode !== undefined) args.parse_mode = params.parse_mode; if (params.disable_web_page_preview !== undefined) args.disable_web_page_preview = params.disable_web_page_preview; if (params.reply_markup !== undefined) args.reply_markup = params.reply_markup; return new Promise(function (resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'editMessageText' }) .then(function (body) { return commonResponseHandler(body); }) .then(function (data) { resolve(data); }) .catch(function (err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: editMessageCaption * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * message_id Required if inline_message_id is not specified. Unique identifier of the sent message * inline_message_id Required if chat_id and message_id are not specified. Identifier of the inline message * caption New caption of the message * reply_markup Additional interface options. A JSON-serialized object * for a custom reply keyboard, instructions to hide keyboard * or to force a reply from the user. */ this.editMessageCaption = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.message_id !== undefined) args.message_id = params.message_id; if (params.inline_message_id !== undefined) args.inline_message_id = params.inline_message_id; if (params.caption !== undefined) args.caption = params.caption; if (params.reply_markup !== undefined) args.reply_markup = params.reply_markup; return new Promise(function (resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'editMessageCaption' }) .then(function (body) { return commonResponseHandler(body); }) .then(function (data) { resolve(data); }) .catch(function (err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: editMessageReplyMarkup * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * message_id Required if inline_message_id is not specified. Unique identifier of the sent message * inline_message_id Required if chat_id and message_id are not specified. Identifier of the inline message * reply_markup Additional interface options. A JSON-serialized object * for a custom reply keyboard, instructions to hide keyboard * or to force a reply from the user. */ this.editMessageReplyMarkup = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.message_id !== undefined) args.message_id = params.message_id; if (params.inline_message_id !== undefined) args.inline_message_id = params.inline_message_id; if (params.reply_markup !== undefined) args.reply_markup = params.reply_markup; return new Promise(function (resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'editMessageReplyMarkup' }) .then(function (body) { return commonResponseHandler(body); }) .then(function (data) { resolve(data); }) .catch(function (err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: exportChatInviteLink * PARAMS: * chat_id Unique identifier for the target chat or username of the target channel — User or GroupChat id */ this.exportChatInviteLink = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; return new Promise(function(resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'exportChatInviteLink' }) .then(function(body) { return commonResponseHandler(body); }) .then(function(data) { resolve(data); }) .catch(function(err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: deleteMessage * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * message_id Required if inline_message_id is not specified. Unique identifier of the sent message */ this.deleteMessage = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.message_id !== undefined) args.message_id = params.message_id; return new Promise(function (resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'deleteMessage' }) .then(function (body) { return commonResponseHandler(body); }) .then(function (data) { resolve(data); }) .catch(function (err) { reject(err); }); }).nodeify(cb); }; /** * METHOD: sendGame * PARAMS: * chat_id Unique identifier for the message recepient — User or GroupChat id * game_short_name * disable_notification * reply_markup */ this.sendGame = function (params, cb) { var args = {}; if (params.chat_id !== undefined) args.chat_id = params.chat_id; if (params.game_short_name !== undefined) args.game_short_name = params.game_short_name; if (params.disable_notification !== undefined) args.disable_notification = params.disable_notification; if (params.reply_markup !== undefined) args.reply_markup = params.reply_markup; return new Promise(function (resolve, reject) { _rest({ method: 'POST', json: true, formData: args, uri: _baseurl + 'sendGame' }) .then(function (body) { return commonResponseHandler(body); }) .then(function (data) { resolve(data); }) .catch(function (err) { reject(err); }); }).nodeify(cb); }; // Start updates retrieving loop if (_settings.updates.enabled == true) { // Cleanup webhook in case if was set before this.deleteWebhook() .then(function() { internalGetUpdates(); return null; // avoid runaway promise warning, see http://goo.gl/rRqMUw }) .catch(function(err) { throw new Error(err) }) } // Start webhook if (_settings.webhook.enabled == true) { webapp.use(require('body-parser').json()) webapp.post('/' + _settings.token, (req, res) => { var data = req.body try { //('forEach' in data) && data.forEach(function (item) var item = data // Update events self.emit('update', item); // Inline events if (item.callback_query) { self.emit('inline.callback.query', item.callback_query); } if (item.edited_message) { self.emit('edited.message', item.edited_message); } // On inline query is received if(item.inline_query) { self.emit('inline.query', item.inline_query); } // On inline result is chosen if(item.chosen_inline_result) { self.emit('inline.result', item.chosen_inline_result); } // Notify subscriber if (item.message) { self.emit('message', item.message); } } catch(err) { console.log(err) console.log('[TelegramApi]: failed to parse update object') } res.status(200) res.send() }) var privateKey = fs.readFileSync(_settings.webhook.privateKey, 'utf8'); var certificate = fs.readFileSync(_settings.webhook.certificate, 'utf8'); var httpsServer = https.createServer({ key: privateKey, cert: certificate }, webapp) httpsServer.listen(_settings.webhook.port, _settings.webhook.host, () => { // Started listening // setWebhook var url = _settings.webhook.url ? _settings.webhook.url + ':' +_settings.webhook.port : 'https://' + _settings.webhook.host + ':' + _settings.webhook.port console.log(url) this.setWebhook({ url: url + '/' + _settings.token, certificate: _settings.webhook.certificate, max_connections: _settings.webhook.max_connections, allowed_updates: _settings.webhook.allowed_updates }) .then((d) => { console.log('DONE') console.log(d) }) .catch((err) => { console.log(err) //throw err }) }) } }; util.inherits(TelegramApi, EventEmitter); module.exports = TelegramApi; telegram-bot-api-1.3.3/package.json000066400000000000000000000017741344550264400171550ustar00rootroot00000000000000{ "name": "telegram-bot-api", "description": "Node.js module for Telegram Bot API (https://core.telegram.org/bots/api)", "version": "1.3.3", "repository": { "type": "git", "url": "https://github.com/mast/telegram-bot-api.git" }, "main": "./lib/telegram-bot.js", "keywords": [ "telegram", "nodejs", "node.js", "telegram bot", "bot", "api" ], "author": "Max Stepanov ", "contributors": [ { "name": "Max Stepanov", "email": "mast@imast.ru" }, { "name": "Stanislav Gumeniuk", "email": "i@vigo.su" }, { "name": "Ali Mihandoost", "email": "i@ali.md" } ], "license": "MIT", "dependencies": { "bluebird": "^3.4.0", "extend": "2.0.2", "request-promise": "4.2.2", "request": "2.88.0", "express": "4.16.3", "body-parser": "1.18.2" }, "devDependencies": { "mocha": "5.2.0", "should": "9.0.2" }, "scripts": { "test": "./node_modules/.bin/mocha test" } } telegram-bot-api-1.3.3/test/000077500000000000000000000000001344550264400156355ustar00rootroot00000000000000telegram-bot-api-1.3.3/test/assets/000077500000000000000000000000001344550264400171375ustar00rootroot00000000000000telegram-bot-api-1.3.3/test/assets/image.png000066400000000000000000001204241344550264400207320ustar00rootroot00000000000000PNG  IHDR~uIDATx^kLUᗛG^ XQ -{ްThuTZR>hqLɌu*aF(שʴ֌SXeqQkyN߬'N9$<$Z{0M",6#{᮱3VnJo{D$ }8ca-'(5<5gs"!8\]roosD4@Dl%f,+egX9s"D4@DCuީ}JU/D4@DV&iab̅b:pet`o1]_>VD4@D?zp8 0mmmhnn6iVʘ 4VfB D\\1b:dz޽{2\|wA' "FGqcbb0~xL0 b𧆆3HիW؈8a⦻h|tw|ȑ2e )))G09NTVVpd'0HDD$0|g7M>CVV`g. eee8z(Μ9ϽW}h|d;w.f͚e6{gϞԩS8v\?hd̉>D4@D>fX̙39993f *?~'OÇa <+]sRjʕ6mBy}vsB޾ ],^rss͊#22G[x,O 5z+vD7""X`9E)bǎx%,-aA/! "saHOOիjl۶ H-6' "RֿUҥKdsW:>o>ݻ׬L,J^cو^,,,Drr2?q6ol>b? 6" "aA8VZe吮CRTTdl~NCD4@DVv}fd8r,rZd"X4YlϟC90D4@D< G^^f̘ˈ$ʮ]FeC{l ዁Άĉ愖\lۍ X8<̞=<ٳgN>  "žb߂I&Y7ůot,pMd"&+jM 6;N̕7n꫾{S]4@DJi^PPT\v|ĢM$ ssj…<9pJKKa +H,e <yds*(h|kj ED+fxXx1tb>~X^DDf 2uׯ_oiiiM̯Eh=+Z܋/"N6E@DrNff>+H8& D$հ9]ֆ'[l1,{  "k{0vXsHqq1jjj`!'{]@avpM_ 6VpVľ\hdډl+DD5t ::ڦDbbb'ٟ}P @~9@ğ$umh `S;G*}jhCx=6(6 6<< ,U" ÞY;68p BV F~gD4@Dҽ]6S~ɓ'I=z4G߿c?a" <%o  . G"""`ֆǏE8sƒB),^.t5 !nBYY;_p@Db͛7͗_mCWx,sH }#yR'MMMf#fw#,^"%%'{̆MI>HpJVTWW#DV @Dࣤ$7#ׯ_7{RA @D;inn6( @DvaÐrG`fKwVɈGH]]ه wAMt; :;hРc|6.D+ 4DFF¿ɒ ad4@DXzSYaaa8Na#:") СCw}a3l+BLv 2d-Z۷e^6! BBv,[֚V6uGHYvB="ZypxmK Bț{wuuL:C6 *B[ ? u[]b=VkmVumk+mݪH( k.eLf|~{sLH,r|Nd$ +}ũ[]gyK9l b!`׮]y\5͝;g}Vn] W_}Ul-(x!K\Dx#Pvίk)j~ߊA]3G[ , r)*pK/cÆ \tE܂o~ |;ߑa%dR( ya;rDO>)xkf#  )*@Չ.f_#DMDuz \6AٳiӦET("-#lԩS 7ŨQD7:"Hc5 nڴ1cp?9n/0!KGnVyc=&FOqD"WC}HbR(AE~"(N}}=~%LH"{eO[Y@3C E6:ؾK/%k!vꮻ s=0Mv1d((`'.Zp-9Z2o$~yJF  ;?CZZ^{-LN-((2H#X&PT(*@.Ѡl͛7/wy߿5kֈ^pBd)BYGQ1Lr EdQLEG}T|weرb`~a1_3n"T EH oVi@zdeƌ駟&''gl۶M"B7}Xtˣ'N䩧 yUrUWEo(*@ 3g iK2@Ĉv٬;3e˖#rƍyW?L2qea֕We„ Q EH ˋVX~#N:$y)77W⋩$82dy{lዲn[?+p{7ESӑ=#PBє)%%K9T ("}]K+@Pd7wkk+}':?"<㲾qe\ "yf1%Xlɟ5PguVq8`DDM DQ{Mz@nXD{#]탾mq v XVY,h9_ cV ҉D(o%aܸq]oСC:\#KQɭt .В#G>ASSSCbq5W(*@QIIoP6UWWYW^yV!b(syHl6^e?~CCJ)CQ y j;Mv"@|!$_2tD* s1 ,F7A+OE+ Ema b z}DfvAzB E 9ש¹|u1w\?CȿO4D0! v=(rIƽm, CtY浾r+>V^ 􅘸j7.+@@"  1@DY^GbL ,0ui0a5QTD4 w^? tq(R$8M%OUɚՖ0 QT(Pa@"()˃9b* 6,>ޝ5Dkwinnph EDn!5! F,VɁ?D1Q!Yz5@v?3C Xu?o2@}bEQ%gDi">/j!N\UϐOrʹ:;i QյxL3Ʋpq\4{6"jQT/|s?Sz n{V##@" ־[RČDLX-F F|tIÃCytx{L`~zxB 񱱱l_`Pƹ+J)P/{;Q%ƽM Dt]rUЭ#wW9^b7[8_14yIxȋX@,\8酢DWT D7,whЈ9=}5<4 RˬgD^5c0it `J!&*eLQ#ϩ'% A7&?=/myM֣wz腼=ƎK0C$QuQ]&E_dmF(4`TnCfMn2R[HMj/N{thiܒH-N}1%sRrg]99 ?Pn;5kkx@> ZړO>8qs8~|hIve54oH瘯uMI-d.bdLG? yTV5l1X5SOwDIN 0Lo&sBrRLhNAN|4fBM17A@ y=m*҈qȏ57xHFҨN2r4WG* ƒ%KXbAǨ|A" 0\p~k%gֵߟMg`u1nd9cБw'C Ey8[ne„ [>ɉL(.Ӊ `p=;yl뵲ý.eIHwY#H."ChI駟fٲeDGPT(*@$ľ =xF|RۘX\}&U1'!Ϲ䷠w`MDjcERI8SpAx!G ,$DĔ^9fv#<d73v5b`ս ͟5㎑!ܐ@׏-?dUrbp[gQ(o/G!{v;^lFTv0phJ'$ZiOGd}l^PہrT(ʿ至'r)̯nCED~`h w}ϺPD[S7Kщ82Q7 ܱܻLc=4.pqA{7s;ϓkY1Q7 Ʌ b4_x0a|)Vkk`H!j>΁ 4tb&wُa&;RT( ` A(ΎG`14 HoY@ /okeգ6afHQ(3/\5ѣ+ɱ)!a\W>{a _žU/Ok0w儦NQtc͹£\[߫xНV pFHքp{@苢DQriLf'+ºc]`JЃXU!ZhxA h4%Pw$E(ƝVN[cnT9'@ DOb1`Ss(c<|+=t8& Da+5ӺUvP-K+20  C.I жˡ=zCu xq2z`ʱU&vo<b1n   d'+gp5!60ߖ"\/xtpUaex; TOȞL:=`BYÛDrV[S>ى`Q(GɀOn-7Zw-t0z2#A [1\7JGWfrFHh, &l."> E@Fec0$$uj} F`Lc bAg_#SZnۆ!nr[+M6&b541ZdH7 EmaDx[*cJc v𶇳GCnWޞCՁ4ʈi$vRD"`$pAQ-,Gv㞎uНqx0d\-| -umʧܣ1bj5iy~} ]ӫDQ;54[H#h1- z'n=HZ^_eQ/]ɵ䎱Z}mfpGE<QT48xI&y^JZAwn40oC>&'@ <_6uefg)-*y(otLEM3ɊhKHAV ̉ҁN4`ߏW(ۑEn[K1&/#O"{TcǙԗ]'e w9Q #J xn/&,M^b OH T ZY*51VPŌϺveC#@4^E? @?n(&5>hd5RX\Kl;keͧ|W&;3UçU;6`ѴeDpR(@=rV&:fJÚ# $B 9~쫬6Ϊ .5`V--<~h$\ K@B%33I5 Y\c=A͂ݱrm X%?@BmÑdʶgخ[uESk6mG7&S5[]MDNQ+E l%7efeƷ3ʥ' BbF-s[1Z d1}"5&bUүuMNwɇv3v# Mq~4 RrZLF4m`VD#_S+ +o`xkطo/Ql1' p !YK·M=ZEvQ#3)ݙ%V}&Ot]|WG^7a0,MIi#SgV_eP*k=[F sGᒋ"Jmf:'  wbB秷G>Guh&O9rP#Tt&W=i$X~diYw[U6RJ2 {Lcyӳ$ x EY#Dw..\qy}OZ '#V GR/OB͝!N74,)Nߤn*?[iG'ZL!9ɭx%$J9i^N*@e+0=0idi*û1hgܬr2 Ap(Ȏ63<"dDf+?wy 1:x!gQ'0ǵ lf"c{Ҋ@ !Pݳk*Oem:z*@ !DソTzh>߁ `M^ľ= Zt)ZG{opD;j*@%h4B'b4^}+x<(PSE&Ah']=p咋.p:. E)>^F6ѰI|đ)=,80zɧP2]wIӖR^YE{pz 8T(|`%!2L|:z3,7l4M&>Û)]ա#n`g{H8ʏTz %\LUe%Qn Ex+LQaInxM`R]?7%wUg >i=Ҳr쭭׮_Is3D p!2 [4< 1ikdTɟX!GDccv~ѧ11''멬jGnf˖~ TMTQ("Mtg 4+^=ˁOnUiz4@dch:@W9 o24@;>\?8UW^Ngg'Q^ EF?lÇ`-Oz\)[.ƟDx*d`hݤMOdc`IIm0o(N{CT(J,gԄd"핻9p6`ه1y#nGdRYY^aas*])++P577K\D< r:)d5b5,Wc%_|DxM!wbZdD}\@WcNgË́CEEFB$s'BMn" E=eNG ^ֲ/*)1gV`:[ir[G6gv;ee02컷yüש!D(ZZ9yżf" Ey XD?}cÇ'TEgwtmrnZc04miTnCWqY>q@^y)97ND$Eu@:~&OD?ɺH;9cnDyS0{&GsEnS5%Gw\q[hf :W-GUN{ Q&;+?d֬Yv+|l>BN(W$qCh n$EΜk Grt'_>Q_NQT( b':J^4->t"k#iX~s &v4q4Re4QxLFdϾa9N%eA7666rEt8D&`*PJPT(F % {y.]@;DWhto'1wbx0tG"4j1:[,-f$ SI(&#.s$[Ss3U5?<^+яOoDEr/G?nn_iok!! iD#Gkkhϣǯ m`!arϽu;i+ق_?p(SJE$.#Nf ҹ]n =߈~x3W %(ʕL?y:CG~U{EՉ! <|9ל/WhF0`y8}@WUe޼䥒PzQA4%4AUQD:m D E@ (:*,!!{e(.ه o;&2u*}zZ_}^8Y{~QhLp37骴#Gš";l4@@oLPN֯ GCy<`Xs=E$ҁ끽Fif3H>ٲ*U`/vb=qǟ $U *)iw|5׎ XNi'qK.[u~y}y b;JqnL}u= QQ>ciӖҥKhoUotLQ\m۴ů.?pz̞53z   s&oors7KdocZL6ejup?⫱cFƍګ4@T`'PތŋЬysC9" }M9 h dSdJD*3&G^wms%Oޞ8q_eeeѹc?o Ƚ %\mj1I=)H8Z`) OL԰a#ʕ+Ovv.WΥ+q M sI ~Cj`cf*PD 8>m{QL M}6 @C1b0Q %<YvHJo,.xxbLDMa ;GDtS4oт_\^\l#,u+sKSrX\.FyXzc妮]}~=C@TU`GY J\ry48olvĴ:,[;;u&?.hA=AFFV|m@ *̷*<"""ܱYYل_5R10 r뀶XS<;Hb4@vͭ=oZ[ " j$0ٓː[nw<9N>:&wCmVv͚50233 u;R^ٳءaÆ^b: 0* |TBիWge4###̒އ>XO_]B D}Ś5opiↅ\Ts3m~V j40:5Zlj uz9{L_9նM+=J1 x ZgAMF7PRᄄXDy62 C2QuS$IT( 5lH[o'S >-[(KC@ :6Ay񒑙!oizQ@S  DH.# ?Q?_H++t_EwxE~9A-E-q!\W.'5(n l'NM7uOH]<z=x'NsNrQ/c4@즞ŏVbŊ 1 "Iħdy*h|b ҥKM8!Ab]pI6TP ?j߾LJ. #B`ނw8) HojXMMFàA$L"8%K3a8Pj4@س̡ifZ6))<أrE1ͪ *^\ J)5DC_kM*Wf͚T23!|(4@̤:uŗ^,J=zT־8pJ*`%6Mt3,׭b& WB w$%%1-Jq&M2g<צԩWٔWc̚93gnhRTTw81cR? PT j֬EUt܅ D=:#^~+){nnQظq 3+ X`)+VVSꉿ=ŋRÆ gC%Ny3gH=X&sZ(umСΕ:u r:rhMH*C),l IM%11 (5g7wIM1k6aaaWF η~CHj 0qCnJtLɓ'1C]yɄtֈؼys4ԟ#V#XvS%J`0hdd;&St)ʖ)CbbY%%R\9*T(O\"U*WX_s}~pǓ܏n=bիV1ϴצʔH Q+KQN\əorΝ$4#" w}|[];gϞ%#3lUO*$-- <8lX@|{31b$SI2eH]|Zsp7K`υE@җIFfg38!,z7e LO஻d4,x5@.zoc_O>\d82ITd+$ !˙3g9}挜՟ 1 qV_|] '' pw> BVo^}'ۧ>WI>\!IXDEEނTOP9^,fΘ/`07ޘA~z4vi(׀!O/fl߾_=3uߏbc N<%IiӺYYY)::~|kZ5&vҁ WUUch[DD1Q(#= uʉ'9y4٨#XzfKHH`ȑ#uNBr @veD;ng׮/ҭ[w;$0bc(Az N<\>ah8wޛطoEqY7@pfe j֪Ų)d4bKP"&ôz<"ǏDW0:w{b>=5 |';hwgeՀߋ_@,xeT:uD=oWѱcG,$RbKHXJ׉'8~`YLe C͛O&M.Xg-~7KVGܹ3kצ)<-}!7˔.eSxpʔ.-?*=]^dU<0Dѣ@ūQT,HʊUF{Ռhݦ!ST%H)cGeK`=z+W` O"@={̩Sp * ʊO3gͦeV,_1UyoHT| (]VL;z'Oh>3mPǢ%JE.K?a55oUYsҹ{}o2,|1)UTLT#r$~˅Vpa cǎae8`~p |c]+3-G)[oc8n|ɒ@rc3;wyi3rm!@Z\.:u7xD0wq'2/GÆJF{'k=zL.%:ݱchڤ'U'[:0a< XZ߷xo.YG+>shGU2e'((~aW#O?J1C. 䦮v7d:8Dhg˨S>pJ/Ẻu)ل[*!WEyrr8|,pt|Sְ[cN:5.4__n݊4> Q-MX{㏟yhX|աC2_HS_%Kɽ0j eՀl޲S9G1d8VT(Q@bR#F<nڴiE 1|$ K*KR%dc[&qk{3))ׯ?h׾=III8H}pDẂX5+WdϞ=K׮Ԯ]UIe%<v忻Dz;$c%6ٳN^])+qxڥ?PF +:*Ax9r(O!n7uj+Vk:qp:D5>MdYv+V0j|$&UJK?.~7cϓˌ2Yok$}\{u>^{"0 AHM!ȈnDdQ39-\>=(V1sڵC{C8yZ}4hPiN-R3t )3pzQRŒ~jҤ)vXjjp믿vZ3 ]0X*M<_y]/_PQ\Rr'RJUY<6mI8L)Mի>nv~#ٰq#eʖܴ|RrПYcF>֤媚5۹۝z tCwtbbb.XYQUXQnTL:ɥw,Y.J)yu4َ`aȠ%TcfXn L a@,лODx԰p=^H0<顔;Ҏ`U)aVھsN>MTThѲqn@撾 ,x]|$!C&4,TC|DL ۛ.W(WתŮ]ܸm۶\+[lJ  q %HKKcMLTTdA87R2i3ԑ_Xpʡq WɪV 0 [r侇R2wpkmHrzuG0mp< Gϵ-å#9^zA#2fR!!2d?ˠ&ԭWωRj8SpGo߯|{i.o722ҲP**23g6Uv걸oQrePn=(h ,q@%ݺ [_UTm%o /QJ?aWTPQ>pYmT\%W/gi\-ZtR(ʈ>\K\R ;ckUT'@uasoAE²@w,УgpQ=}IT]ʕ+ {Tuz@I@?\rA)9#ذ~g7r3%Kd#,,TQ0 )WZj8 hq@Nrr]n-޽{I8^(6T vn.VjUTYsy'`pY6oc#M4K,LP*\La~23" E``ҥвeK m;>GK:|)%Vadfea '_~9fjժPWkYannu9i;Ur>#UHa#ii\L2t@V  o ܹ#mڶ%?/tѢbcc arΉ7Z_{ ŠU؍.z`.qőWh(RZ||< 8T-@;擝UJIC5q访)FN+ErIV&}  4+9PW(x'h;+ B&zCHW]uRW RJ>x9@KDZT & -#abXt0:Z+|$gQB\364hX`) 1RD.:TV Uo|̐KC%7)}аaC KU NR*1vHfF&TVT^+ԅŔN^~]ř >3LHr y&}7ePd珱 K(''{֭ر3g` wH Jqxذ~}N rmlW^s+DIoK\"L4iGeqxG ;@4@V^C\Kb$I]HO  iڴ)ݻ7+Wz00LyGvO9}f1b8%K5kYVHig̙ >ŋ f&*z5 ~u мys< 8u[n #S~.l;aEhkڴi4nܘ Ⱦ"mkT~8'BYu|4hЀo>e}=@Ulԯ_&=<?8v(kl!hR,_νby Z8p/rA=}-Ciq }>Rl իWORwaQ0 تeV}wl n?Mok?5[naƌ 7xq㞠xx lذ7hdffbF4@̛?.]?Dt2K@ L4PGϞ=7~<ʕ =grLddTX{^)))̝3G֣ Ni`8~ NCD.Ǒ!dd@YM4iB~=+y/iߕVZԬYSo:u,;w\,Y™ӧB?U ɍ-Zhb0 C{"gKoҥkWLJllE[p>ojnݺiۖmѨQ#w$==]l޼O4vG=8#GD=DBbJNo_Ĝ9s-(+K,Gc<ܙI@BI ( ,k/`C\WwUQWE.((XAR2df~焙Ԑ {2g[~{_|Kdɇwy.^z9sS]~-.\]]C SۤIs]w1|҆Rlg쉏zGLwkr$ VN}-jU!::>F~S'uyj+\r.KΘ1gJKKg4r] {)wj $Sy:呓# W~"M+˽ȑ#SɤI8;e7ڵD @€swiyp9Ј/O${}*H)kV5(]%&6^(+\f-oؿ>)S  G .ȝSO=СCE '$!B+L:{";9vӀ8HVF P*~jS8}G6wnUp`rO_7.[]WRsp#cjp`ID'1:IqWK4q&9Β$w[|ZTr'A7|S&Qhwo7n$5W_} u< /7U(2v2/gR r.9MDG8 ׽Hz$$ Ȣe[8駟q<]v=;: [SͥEF%rM7‹/ ??ӇAGiut =@lHηQ +,<4_Ҳv[*%ED$CExa ^awOk.yaix!Cd#06r#A} t)A RIu7,5&Y,f|a„ &%%Dty+WPEEc*RjN`a7h1a1(=HMY iP=~: (sxwάYߧmy9+V6ihOiutf ح6@qxľOQ8Gjm؜|ugȜ v6;L\r'Fs۝B}M$!uOʩuV^+pc=&m}5i3fu=i@.֕ }jN2'p{֗]8}㻬j|@d GƁ.]J(ݼAeиC$7;K ҉U&OEե%W.sJWde3\II&#`3|A[J.~׉͝p`TP{4}19aDG8ڬdλJt:,|gAP;wv*y|U6n+" F6oޱÄ{v=VE k|JL rt{}=ZLti U{q0d|Kkvmow݋\pc!˹퐡VwGrB vnV_K 5׽}cb:w63C(wq{C̽` 4@뽊&sp]C UOKh># f3qB.V'@|g9K!EF.w?!~zv܉'??+Nvok2Q9aYi fRP" <;YN`"ZcQZgfI7ѱ8v^e  gCdwh8 Y`-pOӛ@$~"WQ'x zw$ 'eHY"+@!/ `˄xtIg ih&O˯"EDG6gD θxQVWq:BTo\A;P쿦^zPIjx-9}gቾYxK>pI$[Hc NH\1GJ"?cPJC,Am|ϓH2Bq*YSeÿl-ttJ.W2j>7ч=<>mP}OÂe;Ǐ/QH82ٴi@onrS Sj1QS፩Xj"p"IZHl2o5RWƾ" | 8}g{EO*~}"S|?W^5*v S vD͐OpL_mZ[IC 0~؜!SaOƉOIY \ˈ ܦ"]Pu]1 aM!4 iNV_C b.@zz :aS^ǀw/@SO=,:|O[ ?饡@={Pv8'&jTHqϨ+rtY"ؠ~|ycy?}pI=S%D3 IMMDm}T~#Щ[ESI$_ݟ sʐ0}ȉjqh]kaPhn{}i@5@& <ߒ)Dh`6F* Q`Ձi8x"n*"@䜈&)*4:: %dyNI\R}S1x# ?d2*)kɉO?vfY$+7LE/, 8p<]I$_HpFR-=Lbz]+!N{d, /fA`7oTjS^Y>8ӦӢ.tt*|Aފq˟u)(1|JDO~C.`f.~ "qRф!5iɉ HMLb75 $pysl!](n>ue(+ ]0 _C#d&ɐr{ R.^oV_͋(z1 pܔQF\8.kM&$DUrbH<6 R7.A@&0!y rG2ד3AZY8G3E`^6 In"7w`@j W%ʕ+ٳwꫣcPr4Pr*BF"2{JKiɮ`rhi*]7׉i9@PUxCH9z E~?+?,v̭҇P(SķU)  xW+hitt%"#Neͺ~kӥ#JQ)WnM0*K$!v4yE!'@$J b~FrAB<n)JU%uv xFĿAٟGJqXGLra 2{m.'xB!DG02ToPq8f/fQ""./LL' @KyJ`?-_40. ?цM/wLB|8wYA޹I+Z 6! Z8Lh%-`.6ߋ 1( v(;XvHu #4۸iAz7صk/6Nc.c'M ʢ| A ó/,pO.\$ʃY2&[ۀ@$ eF~U%7 W- 2jQUi>P-n%X/W*ylU_W $A|Irssil?+::%[rtDZqH],S\Z+[+) ﺌONLt$B?z @H$ ~]Nȅa. IM>%3"{DNo#`9U(ƃ)_1[NHq/b[J!Hv*Jc[j`@lM=V"A`C|qiY"|9d,wy1c ^rŻm*e|D7x7|V`Psxo7pڀ˭1ų{U&z- 1@+dQSL!r\"VHa//jWy/Qr8 0sj?Y|l6)ݻWN; VmNZ'5q;y ]&[`lٺOSZVFG.?&CzI~ @ʢ?8 $+ρ "58h XP b8D )!Ƞ\W@`-t+>gh`]LlJVTi8.M"ROYḅ>*#P_@bb""bpB6`-3)Q!1> fyqp (vdj"|YyםSݫ3zһGgzk?{j?#"|{U*"9t +#VΘ/ԓ2d?9T0xO7y@ࡋD쁲gPt0m1N@E!]{쑖ߒ`ѢeeLqFNOTL#(2Ƥ<[@ Y!}>*ZB8G  s}a 0vwW_p] AP!@P! n)ƒ<U:Jy͛:tDn.Is=#̙3G9"?N^}"@l'#\TPVGYvZ PdJIĹ`쁠C^" ޣī~'+zߘ#44(wgD/++$5h /p! PWW)S駟 uV4جB >#obCzBMRZ@Pa%2hQ=\Rz<!aD`?֕)v%#* ^٥ BƍQ C=$m0{R\;oEUq@pV1tPF&?_ ?q@{`e DP$< n=SRXj{+zD BZ2'SU MS@%K|ً Vgu›Dnz˛Ҥ"3֮]'`)MSX`vFY {z5-A\3 u$#p-_ ,P*g߿;i$>\"Qe"] E޳W@,PWD}կ{Ե:Q>M!]vIrȨU9m鱨V !z>1e'g%P?`r~?eBqbZ9FOߋ)=<#O.orGAJ!g{ghYҙU\|"/Ic=L4b ErN]+J˽xf+dZqf@TUaWE"/PM"Mae31J.=lA @4dHJ(N<c:mf5*-xAƗh%WLL 3gΔCG-ǽ&M_N?t~[#bD nk *X[rk#oBc6((Ab2IGs,YE΀SRf"$^%ρjuQ?+kz 1f^Ѕf ,m{sQ Ut~ꫯ's%ADmp#IV^GVSX@ֺ7x߿]EnOMq "q6m/HqPUUɕW^|#I~cV1@OglN\z^B޶8x x<R]Xv3`>#?I=D L^)x9?Nly/<.<"f퓺CqM>] UUU88pz+˖-`0prcl"w)RPm(APB]{ô&݆Q(ݙ8Xv-͞7Nv$ItP`-S AGJ^[L@pa.;f18/M"YD~ 7_DWODU_Aw+ (Ob- yFHeɬrP=LB6Q䒃or`X>^֭[w#5Հj.e~!4BK"p}jz. {s*tkI>,"K  l`hjG AK>1#Gh,p+v5-+Kg2#zH&24Ls*6?991D6_QE =ޖ4C˒W)40x94D DdT+F BcD~~<4QCxE 0*6Hb:O)rݨ7 ndIjj,rB1]%bT|~b(~MZv1V>G$8dD5kUX&M ~r Џ2 X G6@Xc``ٝrzOQ爋 J"gy) $UU'^u!UeI0`b-y`6G|z]OzH &M"oH*XFºBz( ҏ.#qnW0*<2+w,rPMڕ,u0udD Nr믘\ ~b5@l?r!C?#K"`+u~M.K`gE"N{fإ Xƀ"CH%"DBv6HA^D>{›{79n2َT/y?ĭH vݪGŇZ4GM""zXxPP$ J(O-ty_ρ,PXQ<+ "]Ybw+CdmSDH':}wq F; bWCߺ6@[`-d]jNþuݰZp5]dM"--1S]$D" !(!$)U<;*C ɑL?/9z:"U<(*(@ͣ#(mH:\G.Lr_q=kz8o36M"3@]2fn%R t0T @B>{˟Fˋ(z 햰(;@YAPߘYCMrB]QFXV#'kIkr7Hc@D4\-W<[&_([惠pwmw bMr&iZ4"k}@]X7BU-_Q RG y@f1,_QWӹ坃Tm$"ф&_$"+rsrrDt31t2xJYhцLC.w{hcN_{gY맪7un˶,KV',laIBlda@f%a``b ː dBx_by.k_ZO9}TJVzΩ}*:>'O NE2 &TJ< .!cW9]X>O$=/NKI%`j-_t/M_Yn=l'@rel̆%%%mXS#h7r4a$I"MȲ,Of5_kettx#PX%)BgB?HBYn[_Jt iQE1;b;A /ٵߓȿ' pmQ+b/@8;ޫg$L)# Dr+Ν;vB, Z$7'x+ <LJF^P$YcY}m`{`=U/a⽺Df@ 2mK?Z٧JoVx{6Ddw BrpF ᅴHdt +2WCJfĠP'Iw }G s`KsZ śE ˼Ub#N%9:0]^ae?my Ď~rřgߏDm6> yIJJ/Kb$@潑nFm KB#{ ?w9ɡ(~3J5x v|FPoQ/Oq?]B>8opx!`anAc4.כ0gD K,6?5/C#Cqu趆It/kxH&*m| O# ; d34)9b%RYsN;k%&Ud|b1\ټy3[nٞce@MwP."H7 c376 3.]"ߴ`Y55,x.vRC|/ٚ]+pe/l,qm/jX[΋ߧȖ`0Eqz~\zU7%Ϫh)z{8'8}je7_% IA,%`(7-%ڜ@f/_3LIqU/!F~QP?5lXt)'Ď~A/d&|믿$@\~ZDv$?8'f/:'C?f{l˵39,qV@Rc3JMмv5RԿs-qxE0e_ fM۸9%/86(b]ZF_Q(䀭><>o^F^L! U8y=,o^K,ӽO>UdVVy3G'" !B gci)ؘ + c~WPrxRje-'ȑ#X`4cxWW*w RWfyKvKXNKNEgqkXgzqy ZR;LВ1mbpK/}a $|Mqw ۼX " ;d߀o7r2Er(RUxXÑ {o Xx8O}J서/J<4L XvV`ٗN ?=qT{v*̻0P<)B :Q!zmɎ M I#7Bp{Ȟ]qoca[~wr Z?sSMhIH)}8t0"Gf3> #HV |/9 #/{Iތf-'Ka謒|m*ֿ܏㬲!~k_#Ja"+'ד0 w&yL"Sy:F:2p R #œ@i&<lG%xh6tV)XW s2ϣ9r}F-e,6t͕A#|`Æ vm<7Rr;$s\r48R#;Gey: @+d/#_ikxֈH3 kYY@{Yq ^ e+~KbַW(dj,piݾDNYP{ 4 *}}I*ưN%Z&C |Ur9*Wi6ubV]CoOYz^>^Twxg?k18+?RP@SmFI V\&}_i3DE%TFS\jD/zf8':鞷O*)˚Nqp=<)*(Bnpسg>VO1_ M5J7еGZJXq WápG^i at5`@TQ>*4u&p nru* pL7Zn[z!l zz8 d}A:}F,1[8Lܤˣ3(31BUG&C@{ͥ0:f%YIhZ+*ʕ+ۿ[9bV/o^{8 //GpѴ0|R޹16?andW J黉|g] DLXH1tnwuqDAQ'9 % J ewm+WS|T #R轐jVfB @e6n(d_uY"TVV.37[o%yzzz8]X?i} "K^M}%Vxʴ^}1a_ t٥vZ' Rb xnd[K#9'J~`bւU\)4qKKYr?62~FO%B$˖-K_PRR"LO% ƄhT- '3!>9MÃ^҄p)]fonXExdzO2ؠ"$z'(z* AE(`ĺ@;  3vʤ{RY ~jR)Y<~yU;C4ew%gD(H07K!9S(MuX8, xHtqhF/+e{ 0٨Qv+J sZ`nT+ZW;@GgP^a ġW^o;iy+Autl7 KcyB X1ۓ-o#d棔*/ɕ85X< 焅D(_;"|V .i*h_C<0 ~!zg`5Da=9 !J@qL4TnI hYIPϮD*ńP`bjAT~Vf E7p2U*Le"XIMq-8i*!9f䝡M~,"H"@J.޽vPT8k+ iV`yX2:f_ZGGK5InAMA  oII2npgv0%C@%Q1/04ʵe@u+I҄@CdP| "!6Vc'ѝ D#$x/@Zd$B- H^peYn3 2hjV"KJrhai4~obٗq%ei*q|O=]*X[p2>]=H|Z >L fY{l|4.YA^:4LPQ`^}eN45'ҤLuYgOx9}^&g CF_VlIq )Ce-{cehZ \B+$E`l`jQ†(?#U} `lzYʘjfܪ$fIDؘ]Iɟc_8ŁÏ?*˗XkD`(~L{b=FЛ 2šJebs"C1,rk+J+) gr6X, 5ٞIdf:+%,EI U NDINT ԐUܗ@G<&A nEq"9s@=f*DžPO1.˚|$dd1IlfF2#-t,nDlXuʲQs c_$nDT vq"F@N]"o\d*wX.$bʺ|/fN?E-pY;1P< yȒ L>T|ͅhnzϙTJz;ʙ#'uyLPHid-X9^W Xq]YFP)?ėNq'!+[ WP d%}$ >&"Ȩ/q1 O f(; \ \a oR(}J",D݇V2){b}o[Łó.ajÃf(AĕT$BRZv-=4I盍@ʐ\*XKQ D(?bkھ{7_ sK8ɸƀq@Y ϸ j+4-fIMY:-.I'~IܪˣPʀK58_3#,BP8*r8(5hK@7%> 0j!25?Lj`=S@f64,o"ȓt_Y~ǁ73ǿ |AG G Bo6kpeX IP"ǓtE=WɑIw#'.h0J|"|\]fA@ 2qk*Ļ,)3c#~i w9%v$`#ktJ걆`UcT"\ӓXb9t2<,QG=4d1>7nrUbvpHyNusd*oG 6(B&j,dq!"X:$-4 8f$O^ł`+G8Mrg>pV%cll>ao`AR#Ͽ8$4<f#dRQ )Y4wJ!3w ,4`Ӑ|=X?I*g!,*>yyw 65| IyN! D\ЮFdol^ Ha"Iܐ~UDp)yN s-mKk8MFcJR+.Z: 0m|&\ED.ۆr(M@KV~^F,&>f=u#'F!*H E|N]lƁ<[($K2.~ +ORY:jB Ѣy/_uޥ|-IN`BN'dR (JI~rWN)2eʐdPM *ɔLjxQ g22<[I9.ZH*8:>qr{?AC)\NL-.wB 9 qÁ+gA E#S3r M'UU#0XҺc|ѣhpR 2%]4TO_p@?}>vX)f.@QjjW,AkF"%iYI_+n`?nbđȳ(=0}))txNٳI/@8#L6.&v0orF$vn&2`4-- '\ Q8 $@Hf̓D4Вw&.lb<#YđIMLUU4s_s<7^]/1G ^,j2 W"Z rNUػs%#~2a]*6qAG3BVQW;P,vcw 'DCxHp$䂍ou[)O43tϊHC82 H' (h\KU0K}x%q@% HXprZ 0-RQ}e}{W06'oHp.(y#'j&i\rI 4 $Czغw p7p$ؔnH!.DRa<"cLy11<|JIq%E82_.:O{o:4#@>< `Z[e=V2>%~+v[817_"A-&hñl߱cK+f{x&1/[3!]pؒY:U+~_+A|}M<<&|xs*il&tivmn$|B4DTUj&1|Tc@.3\qPJ`@iCcK/=}0q#8LKř!.$ UtuW>ˣ"3~pp%fdID&kYL<9)P5 3H ;Q2W PĠ ?|o1%hG LBH>%P;)D_! !͸TUC"]̎m8v25Ι!_ܓ{{Q.U/fPWY9qd.3''_#3Hd% ia#ʰqg,iv! 0CS % q4#&`=\ O[]G$z <@(07JqJQDrNb'15y 28C3 |MG?88qmge@qZ@@W1Rqh/:8"> 4% dUU` .1G L C4I xxz5j#1Հ^7;N;?3yIENDB`telegram-bot-api-1.3.3/test/mocha.opts000066400000000000000000000000171344550264400176310ustar00rootroot00000000000000--timeout 5000 telegram-bot-api-1.3.3/test/test.js000066400000000000000000000611501344550264400171550ustar00rootroot00000000000000 // Get main modules var telegram = require('../lib/telegram-bot.js'); var should = require('should'); var path = require('path'); /** * NOTE1: Create test bot by yourself throught BotFather * NOTE2: Your test bot can have any username (of course), but first_name should be equal to "Test Bot" * NOTE3: Create chat with only 2 members: you and your bot */ // Get needed environment var apiKey = process.env.telegramApiKey; // This is your bot token var chatId = parseInt(process.env.telegramChatId); // This is your chatId var forwardMessageId = parseInt(process.env.telegramForwardMessageId); // This is message_id inside the chatId that will be forwarded var api = null; // this is main API shared variable /** * HOOK: Check needed environment */ before(function checkEnvironment() { var err = null; if (!apiKey) err = new Error('telegramApiKey environment variable should be defined'); else if (!chatId) err = new Error('telegramChatId environment variable should be defined'); else if (!forwardMessageId) err = new Error('telegramForwardMessageId environment variable should be defined'); if (err) { console.log('==========================================================================='); console.log('= For testing you need to create dedicated bot through BotFather'); console.log('= Create chat and add only 2 members into it: you and your bot'); console.log('= Export following environment variables:'); console.log('= telegramApiKey: your bot auth token'); console.log('= telegramChatId: ID of created chat'); console.log('= telegramForwardMessageId: message_id of the message inside the chat'); console.log('= '); console.log('= These environment variables are only needed for running tests'); console.log('==========================================================================='); throw err; } }); afterEach(function timeout(done) { setTimeout(done, 500); }); describe('API', function() { describe('wrong configuration', function() { /** * Function resets shared API instance */ afterEach(function unsetupAPI() { api = null; }); it('unknown token', function(done) { api = new telegram({ token: '111111111:AAAAAAAAAAAAAAAAAAA-aaaaaaaaaaaaaaaa' }); api.getMe() .then(function(data) { throw new Error('Function should fail'); }) .catch(function(err) { err.should.have.property('statusCode', 401) done(); }); }); }); describe('functions', function() { /** * Function setups shared API instance */ before(function setupAPI() { // Create shared API for functions testing api = new telegram({ token: apiKey }); }); /** * Function resets shared API instance */ after(function unsetupAPI() { api = null; }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getMe // //////////////////////////////////////////////////////////////////////// describe('getMe', function() { it('should return valid data with promise', function(done) { api.getMe() .then(function(data) { should.exist(data); data.should.property('first_name', 'Test Bot'); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should return valid data with callback', function(done) { api.getMe(function(err, data) { should.not.exist(err); should.exist(data); data.should.property('first_name', 'Test Bot'); done(); }); }); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendMessage // //////////////////////////////////////////////////////////////////////// describe('sendMessage', function() { it('should succeed with promise', function(done) { api.sendMessage({ chat_id: chatId, text: 'Test Message 1' }) .then(function(data) { should.exist(data); data.should.property('message_id'); data.should.property('text'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should succeed with callback', function(done) { api.sendMessage({ chat_id: chatId, text: 'Test Message 2' }, function(err, data) { should.not.exist(err); should.exist(data); data.should.property('message_id'); data.should.property('text'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }); }); // TO DO it('verify parse_mode'); it('verify disable_web_page_preview'); it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: forwardMessage // //////////////////////////////////////////////////////////////////////// describe('forwardMessage', function() { it('should succeed with promise', function(done) { api.forwardMessage({ chat_id: chatId, from_chat_id: chatId, message_id: forwardMessageId }) .then(function(data) { should.exist(data); data.should.property('message_id'); data.should.property('text'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should succeed with callback', function(done) { api.forwardMessage({ chat_id: chatId, from_chat_id: chatId, message_id: forwardMessageId }, function(err, data) { should.not.exist(err); should.exist(data); data.should.property('message_id'); data.should.property('text'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }); }); // TO DO it('verify disable_notification'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendPhoto // //////////////////////////////////////////////////////////////////////// describe('sendPhoto', function() { it('should succeed with promise', function(done) { api.sendPhoto({ chat_id: chatId, photo: path.join(__dirname, './assets/image.png'), caption: 'My Linux photo' }) .then(function(data) { should.exist(data); data.should.property('message_id'); data.should.property('photo'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should succeed with callback', function(done) { api.sendPhoto({ chat_id: chatId, photo: path.join(__dirname, './assets/image.png'), caption: 'My Linux photo' }, function(err, data) { should.not.exist(err); should.exist(data); data.should.property('message_id'); data.should.property('photo'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }); }); // TO DO it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); it('verify photo as file_id'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendAudio // //////////////////////////////////////////////////////////////////////// describe('sendAudio', function() { // TO DO it('should succeed with promise'); it('should succeed with callback'); it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); it('verify audio as file_id'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendDocument // //////////////////////////////////////////////////////////////////////// describe('sendDocument', function() { // TO DO it('should succeed with promise'); it('should succeed with callback'); it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); it('verify document as file_id'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendSticker // //////////////////////////////////////////////////////////////////////// describe('sendSticker', function() { // TO DO it('should succeed with promise'); it('should succeed with callback'); it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); it('verify sticker as file_id'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendVideo // //////////////////////////////////////////////////////////////////////// describe('sendVideo', function() { // TO DO it('should succeed with promise'); it('should succeed with callback'); it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); it('verify video as file_id'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendVoice // //////////////////////////////////////////////////////////////////////// describe('sendVoice', function() { // TO DO it('should succeed with promise'); it('should succeed with callback'); it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); it('verify voice as file_id'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendLocation // //////////////////////////////////////////////////////////////////////// describe('sendLocation', function() { it('should succeed with promise', function(done) { api.sendLocation({ chat_id: chatId, latitude: 56.326206, longitude: 44.005865 }) .then(function(data) { should.exist(data); data.should.property('message_id'); data.should.property('location'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should succeed with callback', function(done) { api.sendLocation({ chat_id: chatId, latitude: 56.326206, longitude: 44.005865 }, function(err, data) { should.not.exist(err); should.exist(data); data.should.property('message_id'); data.should.property('location'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }); }); // TO DO it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendVenue // //////////////////////////////////////////////////////////////////////// describe('sendVenue', function() { it('should succeed with promise', function(done) { api.sendVenue({ chat_id: chatId, latitude: 56.326206, longitude: 44.005865, title: 'Фонтан', address: 'Nizhniy Novgorod, Minina sq.' }) .then(function(data) { should.exist(data); data.should.property('message_id'); data.should.property('venue'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should succeed with callback', function(done) { api.sendVenue({ chat_id: chatId, latitude: 56.326206, longitude: 44.005865, title: 'Фонтан', address: 'Nizhniy Novgorod, Minina sq.' }, function(err, data) { should.not.exist(err); should.exist(data); data.should.property('message_id'); data.should.property('venue'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }); }); // TO DO it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendContact // //////////////////////////////////////////////////////////////////////// describe('sendContact', function() { it('should succeed with promise', function(done) { api.sendContact({ chat_id: chatId, phone_number: '+70001234567', first_name: 'Max', last_name: 'Stepanov' }) .then(function(data) { should.exist(data); data.should.property('message_id'); data.should.property('contact'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); it('should succeed with callback', function(done) { api.sendContact({ chat_id: chatId, phone_number: '+70001234567', first_name: 'Max', last_name: 'Stepanov' }, function(err, data) { should.not.exist(err); should.exist(data); data.should.property('message_id'); data.should.property('contact'); data.should.property('chat'); data.chat.should.property('id', chatId); done(); }); }); // TO DO it('verify disable_notification'); it('verify reply_to_message_id'); it('verify reply_markup'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: sendChatAction // //////////////////////////////////////////////////////////////////////// describe('sendChatAction', function() { // Checking only one action // It's enough for verifying bot API it('should succeed with promise', function(done) { api.sendChatAction({ chat_id: chatId, action: 'typing' }) .then(function(data) { should.exist(data); data.should.be.true(); done(); }) .catch(function(err) { throw new Error(err); }); }); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getUserProfilePhotos // //////////////////////////////////////////////////////////////////////// describe('getUserProfilePhotos', function() { // TO DO it('should succeed with promise'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getFile // //////////////////////////////////////////////////////////////////////// describe('getFile', function() { // TO DO it('should succeed with promise'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: kickChatMember // //////////////////////////////////////////////////////////////////////// describe('kickChatMember', function() { // TO DO it('should succeed with promise'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: leaveChat // //////////////////////////////////////////////////////////////////////// describe('leaveChat', function() { // TO DO it('should succeed with promise'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: unbanChatMember // //////////////////////////////////////////////////////////////////////// describe('unbanChatMember', function() { // TO DO it('should succeed with promise'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getChat // //////////////////////////////////////////////////////////////////////// describe('getChat', function() { it('should succeed with promise', function(done) { api.getChat({ chat_id: chatId }) .then(function(data) { should.exist(data); data.should.property('id', chatId); done(); }) .catch(function(err) { throw new Error(err); }); }); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getChatAdministrators // //////////////////////////////////////////////////////////////////////// describe('getChatAdministrators', function() { it('should succeed with promise', function(done) { api.getChatAdministrators({ chat_id: chatId }) .then(function(data) { should.exist(data); data.should.be.Array(); done(); }) .catch(function(err) { throw new Error(err); }); }); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getChatMembersCount // //////////////////////////////////////////////////////////////////////// describe('getChatMembersCount', function() { it('should succeed with promise', function(done) { api.getChatMembersCount({ chat_id: chatId }) .then(function(data) { should.exist(data); data.should.be.equal(2); done(); }) .catch(function(err) { throw new Error(err); }); }); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: getChatMember // //////////////////////////////////////////////////////////////////////// describe('getChatMember', function() { // TO DO it('should succeed with promise'); }); //////////////////////////////////////////////////////////////////////// // // FUNCTION: answerCallbackQuery // //////////////////////////////////////////////////////////////////////// describe('answerCallbackQuery', function() { // TO DO it('should succeed with promise'); }); }); });