3

I'm using Google Apps Script to create a web app for my Telegram bot. The web app is invoked by the bot via web hook.

My bot has an inline keyboard that you can see in action in GIF 1 below. Tapping the keyboard buttons you enter a code that appears on the screen.

My problem is when I tap the buttons a little faster, callback queries to the web app collide, which results in messed up entry. See GIF 2.

I've been racking my brain trying to figure out a way to prevent instances of the web app from collision, but I've had no luck so far.

I'm posting my code below. Please, help.

The way I see it is that every instance of the script needs more time to complete before the next one kicks in. I've tried using async/await and lockService to the best of my understanding. I've been advised to try and queue the queries, but sadly haven't been able to make it work.

GIF 1

GIF 2

var lock = LockService.getScriptLock();
    
function doPost(e){

      var contents = JSON.parse(e.postData.contents);
   
      var query_id = contents.callback_query.id;
        
      var mes_id = contents.callback_query.message.message_id;
        
      var userinput = contents.callback_query.data;
        
      var message_now = contents.callback_query.message.text;
      
      var inline_keyboard = contents.callback_query.message.reply_markup;  
      
  
      var message_upd = message_now + " " + userinput;
           
        var keydata = {
            method: "post",
            payload: {
              method: "editMessageText",
              chat_id: String(chat_id),
              message_id: mes_id,
              text: message_upd ,
              parse_mode: "HTML",
              reply_markup: JSON.stringify(inline_keyboard)
            }
          }
   
    lock.waitLock(10000);

    UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', keydata); 
        
    UrlFetchApp.fetch(url + "/answerCallbackQuery?callback_query_id=" + query_id);
      
    lock.releaseLock();

    } 
TheMaster
  • 45,448
  • 6
  • 62
  • 85
hey
  • 97
  • 8
  • 2
    Did you write the client side code that makes the request to `doGet()` in the Web App? Do you have any control over the client side code? Can you re-write the client side code? – Alan Wells Sep 26 '20 at 23:12
  • 1
    You also said LockService didn't work. Could you prove it by showing your lockservice implementation and logs? – TheMaster Sep 27 '20 at 03:56
  • @AlanWells, the client side is Telegram, the app. I have no control over its code. – hey Sep 27 '20 at 07:46
  • @TheMaster, I've edited the code above to show my humble attempt at using LockService. It did not have any effect on the issue at hand. I'd appreciate any thoughts on whether it's wrong, why it's wrong and how to better go about it. – hey Sep 27 '20 at 08:06

1 Answers1

4

Issue:

I believe the lock actually works. Problem would probably be the callback query sent by telegram bot. In your second gif,

At the time of pressing 2, 3 and 4, the message it is attached to is empty. So, All 4 callbacks' message.text will be empty

var message_now = contents.callback_query.message.text; 

message_now is empty for all 4 messages and all 4 mesage_upd will be different:

var message_upd = message_now + " " + userinput;

Even if you queue everything server side using LockService, If the message_now supplied by telegram is empty for all 4 messages, queuing is useless to create a concatenated string like that.

Possible Solution(s):

  • Queing the callbacks should be done client side. Only after receiving the first button press' response, should the second callback be activated. I'm not sure whether telegram offers such fine control. But if it does, This is the preferred solution.

  • Use Cache service server side to cache the last message_now for a particular message.id scoped to the particular user. Save it to Cache service for 30 seconds or so. If another callback with the same message_id comes along in 30s and the message.text is empty, use the cached message instead.

    • key: Some type of message_id and user_id combination
    • value: Current concatenated message_now

Snippet:

  let message_now = contents.callback_query.message.text;
  if (message_now === '') message_now = cache.get(String(mes_id)) || '';
  /*....*/
  cache.put(String(mes_id), String(message_upd), 30);

function doPost(e) {
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  const cache = CacheService.getScriptCache();
  const contents = JSON.parse(e.postData.contents);
  const query_id = contents.callback_query.id;
  const mes_id = contents.callback_query.message.message_id;
  const userinput = contents.callback_query.data;
  let message_now = contents.callback_query.message.text;
  if (message_now === '') message_now = cache.get(String(mes_id)) || '';
  const inline_keyboard = contents.callback_query.message.reply_markup;
  const message_upd = message_now + ' ' + userinput;
  const keydata = {
    method: 'post',
    payload: {
      method: 'editMessageText',
      chat_id: String(chat_id),
      message_id: mes_id,
      text: message_upd,
      parse_mode: 'HTML',
      reply_markup: JSON.stringify(inline_keyboard),
    },
  };
  UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', keydata);
  UrlFetchApp.fetch(url + '/answerCallbackQuery?callback_query_id=' + query_id);
  cache.put(String(mes_id), String(message_upd), 30);
  lock.releaseLock();
}

References:

TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • Thank you, @TheMaster. I tried using your code with a slight adjustment and it worked. I then proceeded, out of curiosity, to bypass locking the script and it worked, too. So, to me it only goes to show that no actual locking ever took place, for reasons I can't explain. After that I went on to replace caching message_upd with saving it to scriptProperties because caching is temporary after all, while scriptProperties last forever. It also worked, no locking necessary. Hurray! The only downside to this method is it takes noticeably longer than pulling directly from message.text. – hey Sep 27 '20 at 20:55
  • I will post my final code as an answer here a little while later. Matter-of-fact, I already had this solution that relied on scriptProperties as a fallback option when I came here to ask. Plus another one that would write to and read from Google Sheets. The latter seemed to have the most unbearable latency, but somehow was collision-proof as well. So, I was looking for something more straight to the point, so to speak. Well, seems that writing to and reading from scriptProperties wins the day for now. – hey Sep 27 '20 at 21:05
  • @hey *I then proceeded, out of curiosity, to bypass locking the script and it worked, too. So, to me it only goes to show that no actual locking ever took place* Doesn't prove Locking didn't work. It could simply mean you weren't fast enough to trigger the lock. If the script was long running, say 3 minutes or more, this might become clear. – TheMaster Sep 27 '20 at 21:20
  • @hey Related: https://stackoverflow.com/questions/64093866/google-apps-script-web-app-not-running-asynchronously-as-expected#comment113339319_64093866 – TheMaster Sep 28 '20 at 06:15