Last active
August 10, 2022 10:57
-
-
Save Max-Makhrov/a8f763c6de9640c0e8dcc060dc59a818 to your computer and use it in GitHub Desktop.
Send a message to Telegram with Bot from WebApp. Google Apps Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 1️⃣ change settings | |
// Telegram token | |
var token = "826438459:AAH2hEf7YZq..."; | |
// Spreadseet id for storing memory | |
// Sample: | |
// https://docs.google.com/spreadsheets/d/1pI12qRVgIY7i03tVdd9jNd2GwPDgOi1RLthdpVpdMD8 | |
var memory_sets = { | |
id: '1pI12qRVgIY7i03tVdd9...', | |
memory_type: 'spreadsheet', | |
sheet_name: 'memory' | |
} | |
// test chat id | |
var test_chat_id = -123456789; | |
// 2️⃣ set Web App and save URL | |
function doPost(e) { | |
try { | |
return send2bot_(e); | |
} catch(err) { | |
return getXmlTable_([[err]]); | |
} | |
} | |
function doGet(e) { | |
return doPost(e); | |
} | |
function send2bot_(e) { | |
// check message exists | |
var msg = e.parameter.msg; | |
if (!msg || msg === '') { | |
return getXmlTable_([['🥴 no msg']]); | |
} | |
var chat_id = e.parameter.chat_id; | |
if (!chat_id || chat_id === '') { | |
return getXmlTable_([['🥴 no chat_id']]); | |
} | |
/** lock - use lock to prevent conflicting access to memory */ | |
var lock = LockService.getScriptLock(); | |
try { | |
lock.waitLock(30000); // wait 30 seconds for others' use of the code section and lock to stop and then proceed | |
} catch (e) { | |
Logger.log('Could not obtain lock after 30 seconds.'); | |
return getXmlTable_([[new Date, ' Server Busy... please try after some time', chat_id]]); | |
} | |
// prevent same messages | |
Utilities.sleep(500 * Math.random() + 500); // to let the script run if lock does not work... | |
var timestamp = getTimeStamp_(); | |
/** release lock */ | |
lock.releaseLock(); | |
// try get sent stamp from memory | |
memory_sets.value = timestamp; | |
memory_sets.key = '' + | |
'💬 ' + chat_id + | |
' 📢 ' + msg; | |
var savedstamp = inMemory_(memory_sets); | |
// send result as XML | |
var result = getXmlTable_([[savedstamp, msg, chat_id]]); | |
// use: | |
// =IMPORTXML("https://script.google.com/macros/s/.../exec?msg=beep&chat_id=-154555889","/table/row") | |
// exit function if message was sent | |
if (savedstamp !== timestamp) { | |
return result; | |
} | |
// send message to telegtam Bot | |
var telegtamUrl = "https://api.telegram.org/bot" + token; | |
sendMessage2Bot_(msg, chat_id, telegtamUrl); | |
return result; | |
} | |
// _______ _ | |
// |__ __| | | | |
// | | ___| | ___ __ _ _ __ __ _ _ __ ___ | |
// | |/ _ \ |/ _ \/ _` | '__/ _` | '_ ` _ \ | |
// | | __/ | __/ (_| | | | (_| | | | | | | | |
// |_|\___|_|\___|\__, |_| \__,_|_| |_| |_| | |
// __/ | | |
// |___/ | |
/** | |
* sendMessage2Bot_ | |
* send message to Telegram | |
* | |
* | |
* Telegram Bot API | |
* supports only these tags: | |
* b, i, a, code, pre | |
* | |
* @param {string} msg | |
* @param {int} chat_id | |
* @param {string} baseurl - telegram url | |
* | |
*/ | |
function test_sendMessage2Bot() { | |
var telegtamUrl = "https://api.telegram.org/bot" + token; | |
sendMessage2Bot_( | |
'Hello,\n' + | |
'<b>bold</b>\n' + | |
'<strong>strong💪🏼</strong>\n' + | |
'<a href = "https://twitter.com/max__makhrov">url</a>\n' + | |
'<i>italic</i>\n' + | |
'<em>emphasized</em>\n\n' + | |
'<code>code\nis multiline</code><br><br>' + | |
'<pre>pre\nis multiline</pre>', | |
test_chat_id, telegtamUrl); | |
} | |
function sendMessage2Bot_(msg, chat_id, baseurl) { | |
var text = tg_br2newline_(msg); | |
chat_id = chat_id || GROUP0_CHAT_ID; | |
var data = { | |
"text" : text, | |
"parse_mode": 'html' | |
}; | |
var payload = JSON.stringify(data); | |
var options = { | |
"method" : "POST", | |
"contentType" : "application/json", | |
"payload" : payload, | |
"muteHttpExceptions": true | |
}; | |
var url = baseurl + "/sendMessage?chat_id=" + chat_id; | |
var response = UrlFetchApp.fetch(url, options); | |
console.log(response.getContentText()); | |
} | |
// function testtg_br2newline() { | |
// var text = 'hello<br/>there!'; | |
// var res = tg_br2newline_(text); | |
// console.log(res); | |
// } | |
function tg_br2newline_(text) { | |
var regex = /<br\s*[\/]?>/gi; | |
return text.replace(regex, "\n"); | |
} | |
// __ __ | |
// | \/ | | |
// | \ / | ___ _ __ ___ ___ _ __ _ _ | |
// | |\/| |/ _ \ '_ ` _ \ / _ \| '__| | | | | |
// | | | | __/ | | | | | (_) | | | |_| | | |
// |_| |_|\___|_| |_| |_|\___/|_| \__, | | |
// __/ | | |
// |___/ | |
/** | |
* return saved value | |
* from memory | |
* | |
* saves new value if it was new | |
* | |
* | |
* @param {string} id of memory slot or Spreadhseet | |
* @param {string} key | |
* @param {string} value | |
* @param {string} memory_type: | |
* cache | |
* spreadsheet | |
* | |
* @param {string} sheet_name | |
* @param {string} key_tag (optional) | |
* @param {string} value_tag (optional) | |
* | |
* | |
*/ | |
// function test_inmemory() { | |
// /** 1 */ | |
// var options = { | |
// id: memory_id, | |
// memory_type: 'spreadsheet', | |
// sheet_name: 'memory', | |
// value: 'hi', | |
// key: 'test1' | |
// } | |
// var res = inMemory_(options); | |
// console.log(res); // hi | |
// options.value = 'hell'; | |
// console.log(inMemory_(options)); // hi | |
// /** 2 */ | |
// var options = { | |
// id: 'cache_test', | |
// memory_type: 'cache', | |
// value: 'hi', | |
// key: 'test1' | |
// } | |
// var res = inMemory_(options); | |
// console.log(res); | |
// } | |
function inMemory_(options) { | |
// work with memory | |
var getCacheMemory_ = function() { | |
var c = CacheService.getScriptCache(); | |
var cashed = c.get(options.id); | |
if (cashed) { | |
return JSON.parse(cashed); | |
} | |
return {}; | |
} | |
var getSheetMemory_ = function() { | |
var sets = { | |
file: options.id, | |
memory: { | |
sheet: options.sheet_name, | |
key: options.key_tag || 'key', | |
value: options.value_tag || 'value', | |
func: data2keyvalues_ | |
} | |
}; | |
var res = getSetsFromSheets_(sets); | |
return res.memory; | |
} | |
var saveCacheValue_ = function(key, value, memory) { | |
if (!(key in memory)) { | |
memory[key] = value; | |
} | |
// prolongate cache life | |
var c = CacheService.getScriptCache(); | |
c.put( | |
options.id, | |
JSON.stringify(memory), | |
21600); | |
} | |
var saveSheetValue_ = function(key, value, memory) { | |
if (key in memory) { | |
// do not write to sheet twice | |
return; | |
} | |
var ss = SpreadsheetApp.openById(options.id); | |
var s = ss.getSheetByName(options.sheet_name); | |
var key_tag = options.key_tag || 'key'; | |
var value_tag = options.value_tag || 'value'; | |
var tags = s.getRange('1:1').getValues()[0]; | |
var data = []; | |
for (var i = 0; i < tags.length; i++) { | |
if (tags[i] === key_tag) { | |
data.push(key); | |
} else if (tags[i] === value_tag) { | |
data.push(value); | |
} else { | |
data.push(''); | |
} | |
} | |
s.appendRow(data); | |
} | |
// config/sets | |
var config = { | |
'cache': | |
{getMemory: getCacheMemory_, | |
saveValue: saveCacheValue_}, | |
'spreadsheet': | |
{getMemory: getSheetMemory_, | |
saveValue: saveSheetValue_} | |
} | |
var sets = config[options.memory_type]; | |
if (!sets) { | |
throw '🥴 memory_type = ' + options.memory_type + | |
'; allowed types = ' + Object.keys(config).join(', '); | |
} | |
// memory | |
var memory = sets.getMemory(); | |
// console.log(memory); | |
// result | |
// default result is new value | |
// it remains if memory has no | |
// item of this key in memory | |
var res = options.value; | |
if (options.key in memory) { | |
res = memory[options.key]; | |
} | |
// save result to memory | |
sets.saveValue( | |
options.key, | |
options.value, | |
memory | |
); | |
return res; | |
} | |
// ______ | |
// | ____| | |
// | |__ _ _ _ __ ___ ___ | |
// | __| | | | '_ \ / __/ __| | |
// | | | |_| | | | | (__\__ \ | |
// |_| \__,_|_| |_|\___|___/ | |
/** | |
* converts data to object with keys | |
* | |
* param {array} options.data | |
* param {array} options.header | |
* param {number} options.row_data_starts | |
* | |
*/ | |
function data2jsonarray_(options) { | |
var tags = options.header; | |
// loop rows | |
var res = [], chunk = {}, num_vals, val; | |
var start = options.row_data_starts -1; | |
for (var i = start; i < options.data.length; i++) { | |
chunk = {}; | |
num_vals = 0; | |
for (var ii = 0; ii < tags.length; ii++) { | |
if (tags[ii] !== '') { | |
val = options.data[i][ii]; | |
chunk[tags[ii]] = val; | |
if (val !== '') { num_vals++; } | |
} | |
} | |
if (num_vals > 0) { | |
res.push(chunk); | |
} | |
} | |
return res; | |
} | |
/** | |
* convert data to arrays of json grouped by key | |
* | |
* param {array} options.data | |
* param {array} options.header | |
* param {string} options.key | |
* param {number} options.row_data_starts | |
* | |
*/ | |
function data2jsongroups_(options) { | |
var tags = options.header; | |
var key = options.key; | |
var start = options.row_data_starts -1; | |
// loop rows | |
var res = {}, chunk = {}, num_vals, val, k; | |
for (var i = start; i < options.data.length; i++) { | |
chunk = {}; | |
num_vals = 0; | |
for (var ii = 0; ii < tags.length; ii++) { | |
if (tags[ii] !== '' && tags[ii] !== key) { | |
val = options.data[i][ii]; | |
chunk[tags[ii]] = val; | |
if (val !== '') { num_vals++; } | |
} else if (tags[ii] !== '' && tags[ii] === key) { | |
k = options.data[i][ii]; | |
} | |
} | |
if (num_vals > 0) { | |
if (!(k in res)) { | |
res[k] = [chunk]; | |
} else { | |
res[k].push(chunk); | |
} | |
} | |
} | |
return res; | |
} | |
/** | |
* converts data to key-value pairs | |
* | |
* param {array} options.data | |
* param {array} options.header | |
* param {string} options.key | |
* param {string} options.value | |
* param {number} options.row_data_starts | |
* | |
*/ | |
function data2keyvalues_(options) { | |
var tags = options.header; | |
// loop rows | |
var start = options.row_data_starts -1; | |
var res = {}; | |
var keyIndex = tags.indexOf(options.key); | |
var valueIndex = tags.indexOf(options.value); | |
var k, v; | |
for (var i = start; i < options.data.length; i++) { | |
k = options.data[i][keyIndex]; | |
v = options.data[i][valueIndex]; | |
if (k !== '') { | |
res[k] = v; | |
} | |
} | |
return res; | |
} | |
/** | |
* getSetsFromSheet_ | |
* | |
* gets sets from sheet | |
* or from multiple sheets | |
* | |
* @param {object} options | |
*/ | |
function getSetsFromSheets_(options) { | |
var ini_sets = options; | |
var file = SpreadsheetApp.openById(ini_sets.file); | |
var sheetname, sheet, range; | |
var sets_i, data, res = {}; | |
for (var k in ini_sets) { | |
if (k !== 'file') { | |
// get sets | |
sets_i = ini_sets[k]; | |
// get sheet + range | |
sheetname = sets_i.sheet || k; | |
sheet = file.getSheetByName(sheetname); | |
range = sheet.getDataRange(); | |
// get data | |
data = range.getValues(); | |
sets_i.data = data; | |
setSheetsDataEndpoints_(sets_i); | |
// add to the result | |
res[k] = sets_i.func(sets_i); | |
} | |
} | |
// console.log(JSON.stringify(res, null, 4)); | |
return res; | |
} | |
/** | |
* sets data endpoints: | |
* { | |
* header: ['id', 'name', 'sum'], | |
* row_data_starts: 2 | |
* } | |
* | |
* @param {array} options.data - sheet.getDataRange().getvalues() | |
* @param {int} options.row_tags | |
* @param {int} options.row_data_starts | |
* @param {int} options.row_marker | |
* @param {string} options.marker_data_starts | |
*/ | |
function setSheetsDataEndpoints_(options) { | |
// #1. set header | |
// by default starts with row 1 | |
var row_tags = options.row_tags || 1; | |
options.header = options.data[row_tags - 1]; | |
// #2. set row_data_starts | |
// #2.1. | |
if (options.row_data_starts) { | |
// it was set by the script | |
return 0; | |
} | |
// #2.2. concept of self-descriptive tables | |
if (options.row_marker) { | |
var marker_data_starts = options.marker_data_starts || 'rowstart: '; | |
var markers = options.data[options.row_marker - 1]; | |
for (var i = 0; i < markers.length; i++) { | |
// find text in a row where marker is ↓ | |
if (markers[i].startsWith(marker_data_starts)) { | |
options.row_data_starts = parseInt( | |
markers[i].replace( | |
marker_data_starts, '')); | |
return 0; | |
} | |
} | |
} | |
// #2.3. defaults | |
options.row_data_starts = 2; | |
return 0; | |
} | |
// _______ _ | |
// |__ __| | | | |
// | | _____ _| |_ ___ | |
// | |/ _ \ \/ / __/ __| | |
// | | __/> <| |_\__ \ | |
// |_|\___/_/\_\\__|___/ | |
/** | |
* get textual timestamp | |
* from cuttent or from | |
* selected date | |
* | |
* @param {date} t | |
* | |
*/ | |
// function test_getTimeStamp() { | |
// console.log(getTimeStamp_()); | |
// } | |
function getTimeStamp_(t) { | |
var t = t || new Date(); | |
var dd = Utilities.formatDate( | |
t, | |
Session.getTimeZone(), | |
"yyyy-MM-dd HH:mm:ss" | |
); | |
return dd; | |
} | |
/*** | |
* get xml table | |
* | |
* @param {array} data - array of arrays | |
* | |
*/ | |
// function test_getXmlTable() { | |
// var data = [ | |
// [1, 2], | |
// ['ssss', 'ffff'] | |
// ]; | |
// console.log(getXmlTable_(data).getContent()) | |
// } | |
function getXmlTable_(data) { | |
var output = '<table>'; | |
for (var i = 0; i < data.length; i++) { | |
output += '<row>'; | |
for (var ii = 0; ii < data[0].length; ii++) { | |
// <![CDATA[some stuff]]> | |
// https://stackoverflow.com/questions/2784183/what-does-cdata-in-xml-mean | |
output += '<col><![CDATA[' + data[i][ii] + ']]></col>'; | |
} | |
output += '</row>'; | |
} | |
output += '</table>'; | |
return ContentService.createTextOutput(output).setMimeType( | |
ContentService.MimeType.XML | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment