Guest User

OctoPrint-Telegram __init__.py

a guest
Jun 6th, 2019
287
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 53.94 KB | None | 0 0
  1. from __future__ import absolute_import
  2. from PIL import Image
  3. import threading, requests, re, time, datetime, StringIO, json, random, logging, traceback, io, collections, os, flask,base64,PIL, pkg_resources
  4. import octoprint.plugin, octoprint.util, octoprint.filemanager
  5. from flask.ext.babel import gettext
  6. from flask.ext.login import current_user
  7. from .telegramCommands import TCMD # telegramCommands.
  8. from .telegramNotifications import TMSG # telegramNotifications
  9. from .telegramNotifications import telegramMsgDict # dict of known notification messages
  10. from .emojiDict import telegramEmojiDict # dict of known emojis
  11. ####################################################
  12. #        TelegramListener Thread Class
  13. # Connects to Telegram and will listen for messages.
  14. # On incomming message the listener will process it.
  15. ####################################################
  16.  
  17. class TelegramListener(threading.Thread):
  18.     def __init__(self, main):
  19.         threading.Thread.__init__(self)
  20.         self.update_offset = 0
  21.         self.first_contact = True
  22.         self.main = main
  23.         self.do_stop = False
  24.         self.username = "UNKNOWN"
  25.         self._logger = main._logger.getChild("listener")
  26.         self.gEmo = self.main.gEmo
  27.        
  28.  
  29.     def run(self):
  30.         self._logger.debug("Try first connect.")
  31.         self.tryFirstContact()
  32.         # repeat fetching and processing messages unitil thread stopped
  33.         self._logger.debug("Listener is running.")
  34.         try:
  35.             while not self.do_stop:
  36.                 try:
  37.                     self.loop()
  38.                 except ExitThisLoopException:
  39.                     # do nothing, just go to the next loop
  40.                     pass
  41.         except Exception as ex:
  42.             self._logger.error("An Exception crashed the Listener: " + str(ex) + " Traceback: " + traceback.format_exc() )
  43.            
  44.         self._logger.debug("Listener exits NOW.")
  45.  
  46.     # Try to get first contact. Repeat every 120sek if no success
  47.     # or stop if task stopped
  48.     def tryFirstContact(self):
  49.         gotContact = False
  50.         while not self.do_stop and not gotContact:
  51.             try:
  52.                 self.username = self.main.test_token()
  53.                 gotContact = True
  54.                 self.set_status(gettext("Connected as %(username)s.", username=self.username), ok=True)
  55.             except Exception as ex:
  56.                 self.set_status(gettext("Got an exception while initially trying to connect to telegram (Listener not running: %(ex)s.  Waiting 2 minutes before trying again.)", ex=ex))
  57.                 time.sleep(120)
  58.        
  59.     def loop(self):
  60.         chat_id = ""
  61.         json = self.getUpdates()
  62.         try:
  63.             # seems like we got a message, so lets process it.
  64.             for message in json['result']:
  65.                 self.processMessage(message)
  66.         except ExitThisLoopException as exit:
  67.             raise exit
  68.         #wooooops. can't handle the message
  69.         except Exception as ex:
  70.             self._logger.error("Exception caught! " + str(ex))
  71.         self.set_status(gettext("Connected as %(username)s.", username=self.username), ok=True)
  72.         # we had first contact after octoprint startup
  73.         # so lets send startup message
  74.         if self.first_contact:
  75.             self.first_contact = False
  76.             self.main.on_event("PrinterStart",{})
  77.    
  78.     def set_update_offset(self, new_value):
  79.         if new_value >= self.update_offset:
  80.             self._logger.debug("Updating update_offset from {} to {}".format(self.update_offset, 1 + new_value))
  81.             self.update_offset = 1 + new_value
  82.         else:
  83.             self._logger.debug("Not changing update_offset - otherwise would reduce it from {} to {}".format(self.update_offset, 1 + new_value))
  84.    
  85.     def processMessage(self, message):
  86.         self._logger.debug("MESSAGE: " + str(message))
  87.         # Get the update_id to only request newer Messages the next time
  88.         self.set_update_offset(message['update_id'])
  89.         # no message no cookies
  90.         if 'message' in message and message['message']['chat']:
  91.        
  92.             chat_id, from_id = self.parseUserData(message) 
  93.            
  94.             # if we come here without a continue (discard message)
  95.             # we have a message from a known and not new user
  96.             # so let's check what he send us
  97.             # if message is a text message, we probably got a command
  98.             # when the command is not known, the following handler will discard it
  99.             if "text" in message['message']:
  100.                 self.handleTextMessage(message, chat_id, from_id)
  101.             # we got no message with text (command) so lets check if we got a file
  102.             # the following handler will check file and saves it to disk
  103.             elif "document" in message['message']:
  104.                 self.handleDocumentMessage(message, chat_id, from_id)
  105.             # we got message with notification for a new chat title photo
  106.             # so lets download it
  107.             elif "new_chat_photo" in message['message']:
  108.                 self.handleNewChatPhotoMessage(message, chat_id, from_id)
  109.             # we got message with notification for a deleted chat title photo
  110.             # so we do the same
  111.             elif "delete_chat_photo" in message['message']:
  112.                 self.handleDeleteChatPhotoMessage(message, chat_id, from_id)
  113.             # a member was removed from a group, so lets check if it's our bot and
  114.             # delete the group from our chats if it is our bot
  115.             elif "left_chat_member" in message['message']:
  116.                 self.handleLeftChatMemberMessage(message, chat_id, from_id)
  117.             # we are at the end. At this point we don't know what message type it is, so we do nothing
  118.             else:
  119.                 self._logger.warn("Got an unknown message. Doing nothing. Data: " + str(message))
  120.         elif 'callback_query' in message:
  121.             self.handleCallbackQuery(message)
  122.         else:
  123.             self._logger.warn("Response is missing .message or .message.chat or callback_query. Skipping it.")
  124.             raise ExitThisLoopException()
  125.  
  126.    
  127.     def handleCallbackQuery(self, message):
  128.         message['callback_query']['message']['text'] = message['callback_query']['data']
  129.         chat_id, from_id = self.parseUserData(message['callback_query'])
  130.         self.handleTextMessage(message['callback_query'],chat_id, from_id)
  131.  
  132.     def handleLeftChatMemberMessage(self, message, chat_id, from_id):
  133.         self._logger.debug("Message Del_Chat")
  134.         if message['message']['left_chat_member']['username'] == self.username[1:] and str(message['message']['chat']['id']) in self.main.chats:
  135.             del self.main.chats[str(message['message']['chat']['id'])]
  136.             # do a self._settings.save() ???
  137.             self._logger.debug("Chat deleted")
  138.            
  139.     def handleDeleteChatPhotoMessage(self, message, chat_id, from_id):
  140.         self._logger.debug("Message Del_Chat_Photo")
  141.         try:
  142.             os.remove(self.main.get_plugin_data_folder()+"/img/user/pic" +str(message['message']['chat']['id'])+".jpg")
  143.             self._logger.debug("File removed")
  144.         except OSError:
  145.             pass
  146.            
  147.     def handleNewChatPhotoMessage(self, message, chat_id, from_id):
  148.         self._logger.debug("Message New_Chat_Photo")
  149.         # only if we know the chat
  150.         if str(message['message']['chat']['id']) in self.main.chats:
  151.             self._logger.debug("New_Chat_Photo Found User")
  152.             kwargs = {'chat_id':int(message['message']['chat']['id']), 'file_id': message['message']['new_chat_photo'][0]['file_id'] }
  153.             t = threading.Thread(target=self.main.get_usrPic, kwargs=kwargs)
  154.             t.daemon = True
  155.             t.run()
  156.            
  157.     def handleDocumentMessage(self, message, chat_id, from_id):
  158.         # first we have to check if chat or group is allowed to upload
  159.         from_id = chat_id
  160.         if not self.main.chats[chat_id]['private']: #is this needed? can one send files from groups to bots?
  161.             from_id = str(message['message']['from']['id'])
  162.         # is /upload allowed?
  163.         if self.main.isCommandAllowed(chat_id,from_id,'/upload'):
  164.             self.main.track_action("command/upload_exec")
  165.             try:
  166.                 file_name = message['message']['document']['file_name']
  167.                 #if not (file_name.lower().endswith('.gcode') or file_name.lower().endswith('.gco') or file_name.lower().endswith('.g')):
  168.                 self._logger.debug(str(file_name.lower().split('.')[-1]))
  169.                 if not octoprint.filemanager.valid_file_type(file_name,"machinecode"):
  170.                     self.main.send_msg(self.gEmo('warning') + " Sorry, I only accept files with .gcode, .gco or .g extension.", chatID=chat_id)
  171.                     raise ExitThisLoopException()
  172.                 # download the file
  173.                 if self.main.version >= 1.3:
  174.                     target_filename = "TelegramPlugin/"+file_name
  175.                     from octoprint.server.api.files import _verifyFolderExists
  176.                     if not _verifyFolderExists(octoprint.filemanager.FileDestinations.LOCAL, "TelegramPlugin"):
  177.                         self.main._file_manager.add_folder(octoprint.filemanager.FileDestinations.LOCAL,"TelegramPlugin")
  178.                 else:
  179.                     target_filename = "telegram_"+file_name
  180.                 # for parameter no_markup see _send_edit_msg()
  181.                 self.main.send_msg(self.gEmo('save') + gettext(" Saving file {}...".format(target_filename)), chatID=chat_id)
  182.                 requests.get(self.main.bot_url + "/sendChatAction", params = {'chat_id': chat_id, 'action': 'upload_document'}, proxies=self.getProxies())
  183.                 data = self.main.get_file(message['message']['document']['file_id'])
  184.                 stream = octoprint.filemanager.util.StreamWrapper(file_name, io.BytesIO(data))
  185.                 self.main._file_manager.add_file(octoprint.filemanager.FileDestinations.LOCAL, target_filename, stream, allow_overwrite=True)
  186.                 # for parameter msg_id see _send_edit_msg()
  187.                 self.main.send_msg(self.gEmo('upload') + " I've successfully saved the file you sent me as {}.".format(target_filename),msg_id=self.main.getUpdateMsgId(chat_id),chatID=chat_id)
  188.             except ExitThisLoopException:
  189.                 pass
  190.             except Exception as ex:
  191.                 self.main.send_msg(self.gEmo('warning') + " Something went wrong during processing of your file."+self.gEmo('mistake')+" Sorry. More details are in octoprint.log.",msg_id=self.main.getUpdateMsgId(chat_id),chatID=chat_id)
  192.                 self._logger.debug("Exception occured during processing of a file: "+ traceback.format_exc() )
  193.         else:
  194.             self._logger.warn("Previous file was from an unauthorized user.")
  195.             self.main.send_msg("Don't feed the octopuses! " + self.gEmo('octo'),chatID=chat_id)
  196.            
  197.     def handleTextMessage(self, message, chat_id, from_id):
  198.         # We got a chat message.
  199.         # handle special messages from groups (/commad@BotName)
  200.         command = str(message['message']['text'].split('@')[0].encode('utf-8'))
  201.         parameter = ""
  202.         # TODO: Do we need this anymore?
  203.         # reply_to_messages will be send on value inputs (eg notification height)
  204.         # but also on android when pushing a button. Then we have to switch command and parameter.
  205.         #if "reply_to_message" in message['message'] and "text" in message['message']['reply_to_message']:
  206.             #command = message['message']['reply_to_message']['text']
  207.             #parameter = message['message']['text']
  208.             #if command.encode('utf-8') not in [str(k.encode('utf-8')) for k in self.main.tcmd.commandDict.keys()]:
  209.                 #command = message['message']['text']
  210.                 #parameter = message['message']['reply_to_message']['text']
  211.         # if command is with parameter, get the parameter
  212.         if any((k+"_") in command for k,v in self.main.tcmd.commandDict.iteritems() if 'param' in v):
  213.             parameter = '_'.join(command.split('_')[1:])
  214.             command = command.split('_')[0]
  215.         self._logger.info("Got a command: '" + str(command.encode('utf-8')) + "' with parameter: '" + str(parameter.encode('utf-8')) + "' in chat " + str(message['message']['chat']['id']))
  216.         # is command  known?
  217.         if command not in self.main.tcmd.commandDict:
  218.             # we dont know the command so skip the message
  219.             self._logger.warn("Previous command was an unknown command.")
  220.             self.main.send_msg("I do not understand you! " + self.gEmo('mistake'),chatID=chat_id)
  221.             raise ExitThisLoopException()
  222.         # check if user is allowed to execute the command
  223.         if self.main.isCommandAllowed(chat_id,from_id,command):
  224.             # Track command
  225.             if command.startswith("/"):
  226.                 self.main.track_action("command/" + command[1:])
  227.             # execute command
  228.             self.main.tcmd.commandDict[command]['cmd'](chat_id,from_id,command,parameter)
  229.         else:
  230.             # user was not alloed to execute this command
  231.             self._logger.warn("Previous command was from an unauthorized user.")
  232.             self.main.send_msg("You are not allowed to do this! " + self.gEmo('notallowed'),chatID=chat_id)
  233.    
  234.     def parseUserData(self, message):
  235.         self.main.chats = self.main._settings.get(["chats"])
  236.         chat = message['message']['chat']
  237.         chat_id = str(chat['id'])
  238.         data = self.main.newChat # data for new user
  239.         # if we know the user or chat, overwrite data with user data
  240.         if chat_id in self.main.chats:
  241.             data = self.main.chats[chat_id]
  242.         # update data or get data for new user
  243.         data['type'] = chat['type'].upper()
  244.         if chat['type']=='group' or chat['type'] == 'supergroup':
  245.             data['private'] = False
  246.             data['title'] = chat['title']
  247.         elif chat['type']=='private':
  248.             data['private'] = True
  249.             data['title'] = ""
  250.             if "first_name" in chat:
  251.                 data['title'] += chat['first_name'] + " - "
  252.             if "last_name" in chat:
  253.                 data['title'] += chat['last_name'] + " - "
  254.             if "username" in chat:
  255.                 data['title'] += "@" + chat['username']
  256.         from_id = chat_id
  257.         # if message is from a group, chat_id will be left as id of group
  258.         # and from_id is set to id of user who send the message
  259.         if not data['private']:
  260.             if 'from' in message:
  261.                 from_id = str(message['from']['id'])
  262.             else:
  263.                 from_id = str(message['message']['from']['id'])
  264.             # if group accepts only commands from known users (allow_users = true, accept_commands=false)
  265.             # and user is not in known chats, then he is unknown and we dont wnat to listen to him.
  266.             if chat_id in self.main.chats:
  267.                 if self.main.chats[chat_id]['allow_users'] and from_id not in self.main.chats and not self.main.chats[chat_id]['accept_commands']:
  268.                     self._logger.warn("Previous command was from an unknown user.")
  269.                     self.main.send_msg("I don't know you! Certainly you are a nice Person " + self.gEmo('heart'),chatID=chat_id)
  270.                     raise ExitThisLoopException()
  271.         # if we dont know the user or group, create new user
  272.         # send welcome message and skip message
  273.         if chat_id not in self.main.chats:
  274.             self.main.chats[chat_id] = data
  275.             self.main.send_msg(self.gEmo('info') + "Now I know you. Before you can do anything, go to OctoPrint Settings and edit some rights.",chatID=chat_id)
  276.             kwargs = {'chat_id':int(chat_id)}
  277.             t = threading.Thread(target=self.main.get_usrPic, kwargs=kwargs)
  278.             t.daemon = True
  279.             t.run()
  280.             self._logger.debug("Got new User")
  281.             raise ExitThisLoopException()
  282.         return (chat_id, from_id)
  283.    
  284.     def getUpdates(self):
  285.         self._logger.debug("listener: sending request with offset " + str(self.update_offset) + "...")
  286.         req = None
  287.        
  288.         # try to check for incoming messages. wait 120sek and repeat on failure
  289.         try:
  290.             if self.update_offset == 0 and self.first_contact:
  291.                 res = ["0","0"]
  292.                 while len(res) > 0:
  293.                     req = requests.get(self.main.bot_url + "/getUpdates", params={'offset':self.update_offset, 'timeout':0}, allow_redirects=False, timeout=10, proxies=self.getProxies())
  294.                     json = req.json()
  295.                     if not json['ok']:
  296.                         self.set_status(gettext("Response didn't include 'ok:true'. Waiting 2 minutes before trying again. Response was: %(response)s", json))
  297.                         time.sleep(120)
  298.                         raise ExitThisLoopException()
  299.                     if len(json['result']) > 0 and 'update_id' in json['result'][0]:
  300.                         self.set_update_offset(json['result'][0]['update_id'])
  301.                     res = json['result']
  302.                     if len(res) < 1:
  303.                         self._logger.debug("Ignoring message because first_contact is True.")
  304.                 if self.update_offset == 0:
  305.                     self.set_update_offset(0)
  306.             else:
  307.                 req = requests.get(self.main.bot_url + "/getUpdates", params={'offset':self.update_offset, 'timeout':30}, allow_redirects=False, timeout=40, proxies=self.getProxies())
  308.         except requests.exceptions.Timeout:
  309.             # Just start the next loop.
  310.             raise ExitThisLoopException()
  311.         except Exception as ex:
  312.             self.set_status(gettext("Got an exception while trying to connect to telegram API: %(exception)s. Waiting 2 minutes before trying again.", exception=ex))
  313.             time.sleep(120)
  314.             raise ExitThisLoopException()
  315.         if req.status_code != 200:
  316.             self.set_status(gettext("Telegram API responded with code %(status_code)s. Waiting 2 minutes before trying again.", status_code=req.status_code))
  317.             time.sleep(120)
  318.             raise ExitThisLoopException()
  319.         if req.headers['content-type'] != 'application/json':
  320.             self.set_status(gettext("Unexpected Content-Type. Expected: application/json. Was: %(type)s. Waiting 2 minutes before trying again.", type=req.headers['content-type']))
  321.             time.sleep(120)
  322.             raise ExitThisLoopException()
  323.         json = req.json()
  324.         if not json['ok']:
  325.             self.set_status(gettext("Response didn't include 'ok:true'. Waiting 2 minutes before trying again. Response was: %(response)s", json))
  326.             time.sleep(120)
  327.             raise ExitThisLoopException()
  328.         if "result" in json and len(json['result']) > 0:
  329.             for entry in json['result']:
  330.                 self.set_update_offset(entry['update_id'])
  331.         return json
  332.  
  333.     # stop the listener
  334.     def stop(self):
  335.         self.do_stop = True
  336.    
  337.     def set_status(self, status, ok=False):
  338.         if status != self.main.connection_state_str:
  339.             if self.do_stop:
  340.                 self._logger.debug("Would set status but do_stop is True: %s", status)
  341.                 return
  342.             if ok:
  343.                 self._logger.debug("Setting status: %s", status)
  344.             else:
  345.                 self._logger.error("Setting status: %s", status)
  346.         self.connection_ok = ok
  347.         self.main.connection_state_str = status
  348.  
  349.     def getProxies(self):
  350.         http_proxy = self.main._settings.get(["http_proxy"])
  351.         https_proxy = self.main._settings.get(["https_proxy"])
  352.         return {
  353.             'http': http_proxy,
  354.             'https': https_proxy
  355.             }
  356.  
  357. class TelegramPluginLoggingFilter(logging.Filter):
  358.     def filter(self, record):
  359.         for match in re.findall("[0-9]+:[a-zA-Z0-9_\-]+", record.msg):
  360.             new = re.sub("[0-9]", "1", re.sub("[a-z]", "a", re.sub("[A-Z]", "A", match)))
  361.             record.msg = record.msg.replace(match, new)
  362.         return True
  363.  
  364. class ExitThisLoopException(Exception):
  365.     pass
  366.  
  367. ########################################
  368. ########################################
  369. ############## THE PLUGIN ##############
  370. ########################################
  371. ########################################
  372. class TelegramPlugin(octoprint.plugin.EventHandlerPlugin,
  373.                      octoprint.plugin.SettingsPlugin,
  374.                      octoprint.plugin.StartupPlugin,
  375.                      octoprint.plugin.ShutdownPlugin,
  376.                      octoprint.plugin.TemplatePlugin,
  377.                      octoprint.plugin.SimpleApiPlugin,
  378.                      octoprint.plugin.AssetPlugin
  379.                      ):
  380.  
  381.     def __init__(self,version):
  382.         self.version = float(version)
  383.         # for more init stuff see on_after_startup()
  384.         self.thread = None
  385.         self.bot_url = None
  386.         self.chats = {}
  387.         self.connection_state_str = gettext("Disconnected.")
  388.         self.connection_ok = False
  389.         requests.packages.urllib3.disable_warnings()
  390.         self.updateMessageID = {}
  391.         self.shut_up = {}
  392.         self.send_messages = True
  393.         self.tcmd = None
  394.         self.tmsg = None
  395.         self.sending_okay_minute = None
  396.         self.sending_okay_count = 0
  397.         # initial settings for new chat. See on_after_startup()
  398.         # !!! sync with newUsrDict in on_settings_migrate() !!!
  399.         self.newChat = {}
  400.         # use of emojis see below at method gEmo()
  401.         self.emojis = {
  402.             'octo':     u'\U0001F419', #octopus
  403.             'mistake':  u'\U0001F616',
  404.             'notify': u'\U0001F514',
  405.             'shutdown' : u'\U0001F4A4',
  406.             'shutup':    u'\U0001F64A',
  407.             'noNotify': u'\U0001F515',
  408.             'notallowed': u'\U0001F62C',
  409.             'rocket': u'\U0001F680',
  410.             'save': u'\U0001F4BE',
  411.             'heart': u'\U00002764',
  412.             'info': u'\U00002139',
  413.             'settings': u'\U0001F4DD',
  414.             'clock': u'\U000023F0',
  415.             'height': u'\U00002B06',
  416.             'question': u'\U00002753',
  417.             'warning': u'\U000026A0',
  418.             'enter': u'\U0000270F',
  419.             'upload': u'\U0001F4E5',
  420.             'check': u'\U00002705',
  421.             'lamp': u'\U0001F4A1',
  422.             'movie': u'\U0001F3AC',
  423.             'finish': u'\U0001F3C1',
  424.             'cam': u'\U0001F3A6',
  425.             'hooray': u'\U0001F389',
  426.             'error': u'\U000026D4',
  427.             'play': u'\U000025B6',
  428.             'stop': u'\U000025FC'
  429.         }
  430.         self.emojis.update(telegramEmojiDict)
  431.     # all emojis will be get via this method to disable them globaly by the corrosponding setting  
  432.     # so if you want to use emojis anywhere use gEmo("...") istead of emojis["..."]
  433.     def gEmo(self,key):
  434.         if self._settings.get(["send_icon"]) and key in self.emojis:
  435.             return self.emojis[key]
  436.         return ""
  437.  
  438.     # starts the telegram listener thread
  439.     def start_listening(self):
  440.         if self._settings.get(['token']) != "" and self.thread is None:
  441.             self._logger.debug("Starting listener.")
  442.             self.bot_url = "https://api.telegram.org/bot" + self._settings.get(['token'])
  443.             self.bot_file_url = "https://api.telegram.org/file/bot" + self._settings.get(['token'])
  444.             self.thread = TelegramListener(self)
  445.             self.thread.daemon = True
  446.             self.thread.start()
  447.    
  448.     # stops the telegram listener thread
  449.     def stop_listening(self):
  450.         if self.thread is not None:
  451.             self._logger.debug("Stopping listener.")
  452.             self.thread.stop()
  453.             self.thread = None
  454.    
  455.     def shutdown(self):
  456.         self._logger.warn("shutdown() running!")
  457.         self.stop_listening()
  458.         self.send_messages = False
  459.    
  460.     def sending_okay(self):
  461.         # If the count ever goeas above 10, we stop doing everything else and just return False
  462.         # so if this is ever reached, it will stay this way.
  463.         if self.sending_okay_count > 10:
  464.             self._logger.warn("Sent more than 10 messages in the last minute. Shutting down...")
  465.             self.shutdown()
  466.             return False
  467.        
  468.         if self.sending_okay_minute != datetime.datetime.now().minute:
  469.             self.sending_okay_minute = datetime.datetime.now().minute
  470.             self.sending_okay_count = 1
  471.         else:
  472.             self.sending_okay_count += 1
  473.            
  474.         return True
  475.  
  476. ##########
  477. ### Asset API
  478. ##########
  479.  
  480.     def get_assets(self):
  481.         return dict(js=["js/telegram.js"])
  482.  
  483. ##########
  484. ### Template API
  485. ##########
  486.  
  487.     def get_template_configs(self):
  488.         return [
  489.             dict(type="settings", name="Telegram", custom_bindings=True)
  490.         ]
  491.  
  492. ##########
  493. ### Wizard API
  494. ##########
  495.  
  496.     def is_wizard_required(self):
  497.         return self._settings.get(["token"]) is ""
  498.  
  499.     def get_wizard_version(self):
  500.         return 1
  501.         # Wizard version numbers used in releases
  502.         # < 1.4.2 : no wizard
  503.         # 1.4.2 : 1
  504.         # 1.4.3 : 1
  505.  
  506. ##########
  507. ### Startup/Shutdown API
  508. ##########
  509.  
  510.     def on_after_startup(self):
  511.         self.set_log_level()
  512.         self._logger.addFilter(TelegramPluginLoggingFilter())
  513.         self.tcmd = TCMD(self)
  514.         self.tmsg = TMSG(self) # Notification Message Handler class. called only by on_event()
  515.         # initial settings for new chat.
  516.         # !!! sync this dict with newUsrDict in on_settings_migrate() !!!
  517.         self.newChat = {
  518.             'private': True,
  519.             'title': "[UNKNOWN]",
  520.             'accept_commands' : False,
  521.             'send_notifications' : False,
  522.             'new': True,
  523.             'type': '',
  524.             'allow_users': False,
  525.             'commands': {k: False for k,v in self.tcmd.commandDict.iteritems()},
  526.             'notifications': {k: False for k,v in telegramMsgDict.iteritems()}
  527.             }
  528.         self.chats = self._settings.get(["chats"])
  529.         self.start_listening()
  530.         self.track_action("started")
  531.         # Delete user profile photos if user doesn't exist anymore
  532.         for f in os.listdir(self.get_plugin_data_folder()+"/img/user"):
  533.             fcut = f.split('.')[0][3:]
  534.             self._logger.debug("Testing Pic ID " + str(fcut))
  535.             if fcut not in self.chats:
  536.                 self._logger.debug("Removing pic" +fcut+".jpg")
  537.                 try:
  538.                     os.remove(self.get_plugin_data_folder()+"/img/user/"+f)
  539.                 except OSError:
  540.                     pass
  541.         #Update user profile photos
  542.         for key in self.chats:
  543.             try:
  544.                 if key is not 'zBOTTOMOFCHATS':
  545.                     kwargs = {}
  546.                     kwargs['chat_id'] = int(key)
  547.                     t = threading.Thread(target=self.get_usrPic, kwargs=kwargs)
  548.                     t.daemon = True
  549.                     t.run()
  550.             except Exception:
  551.                 pass
  552.    
  553.     def on_shutdown(self):
  554.         self.on_event("PrinterShutdown",{})
  555.         self.stop_listening()
  556.  
  557. ##########
  558. ### Settings API
  559. ##########
  560.  
  561.     def get_settings_version(self):
  562.         return 4
  563.         # Settings version numbers used in releases
  564.         # < 1.3.0: no settings versioning
  565.         # 1.3.0 : 1
  566.         # 1.3.1 : 2
  567.         # 1.3.2 : 2
  568.         # 1.3.3 : 2
  569.         # 1.4.0 : 3
  570.         # 1.4.1 : 3
  571.         # 1.4.2 : 3
  572.         # 1.4.3 : 4
  573.  
  574.     def get_settings_defaults(self):
  575.         return dict(
  576.             token = "",
  577.             notification_height = 5.0,
  578.             notification_time = 15,
  579.             message_at_print_done_delay = 0,
  580.             messages = telegramMsgDict,
  581.             tracking_activated = False,
  582.             tracking_token = None,
  583.             chats = {'zBOTTOMOFCHATS':{'send_notifications': False,'accept_commands':False,'private':False}},
  584.             debug = False,
  585.             send_icon = True,
  586.             image_not_connected = True,
  587.             fileOrder = False
  588.         )
  589.  
  590.     def get_settings_preprocessors(self):
  591.         return dict(), dict(
  592.             notification_height=lambda x: float(x),
  593.             notification_time=lambda x: int(x)
  594.         )
  595.  
  596.     def on_settings_migrate(self, target, current=None):
  597.         self._logger.setLevel(logging.DEBUG)
  598.         self._logger.debug("MIGRATE DO")
  599.         tcmd = TCMD(self)
  600.         # initial settings for new chat.
  601.         # !!! sync this dict with newChat in on_after_startup() !!!
  602.         newUsrDict = {
  603.             'private': True,
  604.             'title': "[UNKNOWN]",
  605.             'accept_commands' : False,
  606.             'send_notifications' : False,
  607.             'new': False,
  608.             'type': '',
  609.             'allow_users': False,
  610.             'commands': {k: False for k,v in tcmd.commandDict.iteritems()},
  611.             'notifications': {k: False for k,v in telegramMsgDict.iteritems()}
  612.             }
  613.  
  614.         ##########
  615.         ### migrate from old plugin Versions < 1.3 (old versions had no settings version check)
  616.         ##########
  617.         chats = {k: v for k, v in self._settings.get(['chats']).iteritems() if k != 'zBOTTOMOFCHATS'}
  618.         self._logger.debug("LOADED CHATS: " + str(chats))
  619.         self._settings.set(['chats'], {})
  620.         if current is None or current < 1:
  621.             ########## Update Chats
  622.             # there shouldn't be any chats, but maybe somone had installed any test branch.
  623.             # Then we have to check if all needed settings are populated
  624.             for chat in chats:
  625.                 for setting in newUsrDict:
  626.                     if setting not in chats[chat]:
  627.                         if setting == "commands":
  628.                             chats[chat]['commands'] = {k: False for k,v in tcmd.commandDict.iteritems() if 'bind_none' not in v}
  629.                         elif setting == "notifications":
  630.                             chats[chat]['notifications'] = {k: False for k,v in telegramMsgDict.iteritems()}
  631.                         else:
  632.                             chats[chat][setting] = False
  633.             ########## Is there a chat from old single user plugin version?
  634.             # then migrate it into chats
  635.             chat = self._settings.get(["chat"])
  636.             if chat is not None:
  637.                 self._settings.set(["chat"], None)
  638.                 data = {}
  639.                 data.update(newUsrDict)
  640.                 data['private'] = True
  641.                 data['title'] = "[UNKNOWN]"
  642.                 #try to get infos from telegram by sending a "you are migrated" message
  643.                 try:
  644.                     message = {}
  645.                     message['text'] = "The OctoPrint Plugin " + self._plugin_name + " has been updated to new Version "+self._plugin_version+ ".\n\nPlease open your " + self._plugin_name + " settings in OctoPrint and set configurations for this chat. Until then you are not able to send or receive anything useful with this Bot.\n\nMore informations on: https://github.com/fabianonline/OctoPrint-Telegram"
  646.                     message['chat_id'] = chat
  647.                     message['disable_web_page_preview'] = True
  648.                     r = requests.post("https://api.telegram.org/bot" + self._settings.get(['token']) + "/sendMessage", data =  message, proxies=self.getProxies())
  649.                     r.raise_for_status()
  650.                     if r.headers['content-type'] != 'application/json':
  651.                         raise Exception("invalid content-type")
  652.                     json = r.json()
  653.                     if not json['ok']:
  654.                         raise Exception("invalid request")
  655.                     chat = json['result']['chat']
  656.                     if chat['type']=='group':
  657.                         data['private'] = False
  658.                         data['title'] = chat['title']
  659.                     elif chat['type']=='private':
  660.                         data['private'] = True
  661.                         data['title'] = ""
  662.                         if "first_name" in chat:
  663.                             data['title'] += chat['first_name'] + " - "
  664.                         if "last_name" in chat:
  665.                             data['title'] += chat['last_name'] + " - "
  666.                         if "username" in chat:
  667.                             data['title'] += "@" + chat['username']
  668.                 except Exception as ex:
  669.                     self._logger.debug("ERROR migrating chat. Done with defaults private=true,title=[UNKNOWN] : " + str(ex))
  670.                 # place the migrated chat in chats
  671.                 chats.update({str(chat['id']): data})
  672.             self._logger.debug("MIGRATED Chats: " + str(chats))
  673.             ########## Update messages. Old text will be taken to new structure
  674.             messages = self._settings.get(['messages'])
  675.             msgOut = {}
  676.             for msg in messages:
  677.                 if msg == 'TelegramSendNotPrintingStatus':
  678.                     msg2 = 'StatusNotPrinting'
  679.                 elif msg == 'TelegramSendPrintingStatus':
  680.                     msg2 = 'StatusPrinting'
  681.                 else:
  682.                     msg2 = msg
  683.                 if type(messages[msg]) is not type({}):
  684.                     newMsg = telegramMsgDict[msg2].copy()
  685.                     newMsg['text'] = str(messages[msg])
  686.                     msgOut.update({msg2: newMsg})
  687.                 else:
  688.                     msgOut.update({msg2: messages[msg]})
  689.             self._settings.set(['messages'], msgOut)
  690.             ########## Delete old settings
  691.             self._settings.set(["message_at_startup"], None)
  692.             self._settings.set(["message_at_shutdown"], None)
  693.             self._settings.set(["message_at_print_started"], None)
  694.             self._settings.set(["message_at_print_done"], None)
  695.             self._settings.set(["message_at_print_failed"], None)
  696.  
  697.         ##########
  698.         ### Migrate to new command/notification settings version.
  699.         ### This should work on all future versions. So if you add/del
  700.         ### some commands/notifications, then increment settings version counter
  701.         ### in  get_settings_version(). This will trigger octoprint to update settings
  702.         ##########
  703.         if current is None or current < target:
  704.             # first we have to check if anything has changed in commandDict or telegramMsgDict
  705.             # then we have to update user comamnd or notification settings
  706.             if chats is not None and chats is not {}:
  707.                 # this for loop updates commands and notifications settings items of chats
  708.                 # if there are changes in commandDict or telegramMsgDict
  709.                 for chat in chats:
  710.                     # handle renamed commands
  711.                     if '/list' in chats[chat]['commands']:
  712.                         chats[chat]['commands'].update({'/files':chats[chat]['commands']['/list']})
  713.                     if '/imsorrydontshutup' in chats[chat]['commands']:
  714.                         chats[chat]['commands'].update({'/dontshutup':chats[chat]['commands']['/imsorrydontshutup']})
  715.                     if 'type' not in chats[chat]:
  716.                         chats[chat].update({'type': 'PRIVATE' if chats[chat]['private'] else 'GROUP'})
  717.                     delCmd = []
  718.                     # collect remove 'bind_none' commands
  719.                     for cmd in tcmd.commandDict:
  720.                         if cmd in chats[chat]['commands'] and 'bind_none' in tcmd.commandDict[cmd]:
  721.                             delCmd.append(cmd)
  722.                     # collect Delete commands from settings if they don't belong to commandDict anymore
  723.                     for cmd in chats[chat]['commands']:
  724.                         if cmd not in tcmd.commandDict:
  725.                             delCmd.append(cmd)
  726.                     # finally delete commands
  727.                     for cmd in delCmd:
  728.                         del chats[chat]['commands'][cmd]
  729.                     # If there are new commands in comamndDict, add them to settings
  730.                     for cmd in tcmd.commandDict:
  731.                         if cmd not in chats[chat]['commands']:
  732.                             if 'bind_none' not in tcmd.commandDict[cmd]:
  733.                                 chats[chat]['commands'].update({cmd: False})
  734.                     # Delete notifications from settings if they don't belong to msgDict anymore
  735.                     delMsg = []
  736.                     for msg in chats[chat]['notifications']:
  737.                         if msg not in telegramMsgDict:
  738.                             delMsg.append(msg)
  739.                     for msg in delMsg:
  740.                         del chats[chat]['notifications'][msg]
  741.                     # If there are new notifications in msgDict, add them to settings
  742.                     for msg in telegramMsgDict:
  743.                         if msg not in chats[chat]['notifications']:
  744.                             chats[chat]['notifications'].update({msg: False})
  745.                 self._settings.set(['chats'],chats)
  746.             ########## if anything changed in telegramMsgDict, we also have to update settings for messages
  747.             messages = self._settings.get(['messages'])
  748.             if messages is not None and messages is not {}:
  749.                 # this for loop deletes items from messages settings
  750.                 # if they dont't belong to telegramMsgDict anymore
  751.                 delMsg = []
  752.                 for msg in messages:
  753.                     if msg not in telegramMsgDict:
  754.                         delMsg.append(msg)
  755.                 for msg in delMsg:
  756.                     del messages[msg]
  757.                 # this for loop adds new message settings from telegramMsgDict to settings
  758.                 for msg in telegramMsgDict:
  759.                     if msg not in messages:
  760.                         messages.update({msg: telegramMsgDict[msg]})
  761.                     elif 'combined' not in messages[msg]:
  762.                         messages[msg].update({'combined' : True})
  763.  
  764.                 self._settings.set(['messages'],messages)
  765.                 self._logger.debug("MESSAGES: " + str(self._settings.get(['messages'])))
  766.  
  767.  
  768.         if current is not None:
  769.             if current < 2:
  770.                 if chats is not None and chats is not {}:
  771.                     for chat in chats:
  772.                         if os.path.isfile(self.get_plugin_data_folder()+"/pic"+chat+".jpg"):
  773.                             os.remove(self.get_plugin_data_folder()+"/pic"+chat+".jpg")
  774.  
  775.  
  776.         ##########
  777.         ### save the settings after Migration is done
  778.         ##########
  779.         self._logger.debug("SAVED Chats: " + str(self._settings.get(['chats'])))
  780.         try:
  781.             self._settings.save()
  782.         except Exception as ex:
  783.             self._logger.error("MIGRATED Save failed - " + str(ex))
  784.         self._logger.debug("MIGRATED Saved")
  785.  
  786.  
  787.     def on_settings_save(self, data):
  788.         # Remove 'new'-flag and apply bindings for all chats
  789.         if 'chats' in data and data['chats']:
  790.             delList = []
  791.             for key in data['chats']:
  792.                 if 'new' in data['chats'][key] or 'new' in data['chats'][key]:
  793.                     data['chats'][key]['new'] = False
  794.                 # Look for deleted chats
  795.                 if not key in self.chats and not key == "zBOTTOMOFCHATS":
  796.                     delList.append(key)
  797.             # Delete chats finally
  798.             for key in delList:
  799.                 del data['chats'][key]
  800.         # Also remove 'new'-flag from self.chats so settingsUI is consistent
  801.         # self.chats will only update to settings data on first received message after saving done
  802.         for key in self.chats:
  803.             if 'new' in self.chats[key]:
  804.                 self.chats[key]['new'] = False
  805.  
  806.         self._logger.debug("Saving data: " + str(data))
  807.         # Check token for right format
  808.         if 'token' in data:
  809.             data['token'] = data['token'].strip()
  810.             if not re.match("^[0-9]+:[a-zA-Z0-9_\-]+$", data['token']):
  811.                 self._logger.error("Not saving token because it doesn't seem to have the right format.")
  812.                 self.connection_state_str = gettext("The previously entered token doesn't seem to have the correct format. It should look like this: 12345678:AbCdEfGhIjKlMnOpZhGtDsrgkjkZTCHJKkzvjhb")
  813.                 data['token'] = ""
  814.         old_token = self._settings.get(["token"])
  815.         # Update Tracking
  816.         if 'tracking_activated' in data and not data['tracking_activated']:
  817.             data['tracking_token'] = None
  818.         # Now save settings
  819.         octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
  820.         self.set_log_level()
  821.         # Reconnect on new token
  822.         # Will stop listener on invalid token
  823.         if 'token' in data:
  824.             if data['token']!=old_token:
  825.                 self.stop_listening()
  826.             if data['token']!="":
  827.                 self.start_listening()
  828.             else:
  829.                 self.connection_state_str = gettext("No token given.")
  830.  
  831.     def on_settings_load(self):
  832.         data = octoprint.plugin.SettingsPlugin.on_settings_load(self)
  833.  
  834.         # only return our restricted settings to admin users - this is only needed for OctoPrint <= 1.2.16
  835.         restricted = (("token", None), ("tracking_token", None), ("chats", dict()))
  836.         for r, v in restricted:
  837.             if r in data and (current_user is None or current_user.is_anonymous() or not current_user.is_admin()):
  838.                 data[r] = v
  839.  
  840.         return data
  841.  
  842.     def get_settings_restricted_paths(self):
  843.         # only used in OctoPrint versions > 1.2.16
  844.         return dict(admin=[["token"], ["tracking_token"], ["chats"]])
  845.  
  846. ##########
  847. ### Softwareupdate API
  848. ##########
  849.    
  850.     def get_update_information(self, *args, **kwargs):
  851.         return dict(
  852.             telegram=dict(
  853.                 displayName=self._plugin_name,
  854.                 displayVersion=self._plugin_version,
  855.                 type="github_release",
  856.                 current=self._plugin_version,
  857.                 user="fabianonline",
  858.                 repo="OctoPrint-Telegram",
  859.                 pip="https://github.com/fabianonline/OctoPrint-Telegram/releases/{target_version}/download/release.zip"
  860.             )
  861.         )
  862.  
  863. ##########
  864. ### EventHandler API
  865. ##########
  866.        
  867.     def on_event(self, event, payload, **kwargs):
  868.         try:
  869.             # if we know the event, start handler
  870.             if event in self.tmsg.msgCmdDict:
  871.                 self._logger.debug("Got an event: " + event + " Payload: " + str(payload))
  872.                 # Start event handler
  873.                 self.tmsg.startEvent(event, payload, **kwargs)
  874.             else:
  875.                 # return as fast as possible
  876.                 return
  877.         except Exception as e:
  878.             self._logger.debug("Exception: " + str(e))
  879.  
  880. ##########
  881. ### SimpleApi API
  882. ##########
  883.  
  884.     def get_api_commands(self):
  885.         return dict(
  886.             testToken=["token"],
  887.             delChat=["ID"]
  888.         )
  889.  
  890.     def on_api_get(self, request):
  891.         # got an user-update with this command. so lets do that
  892.         if 'id' in request.args and 'cmd' in request.args and 'note' in request.args  and 'allow' in request.args:
  893.             self.chats[request.args['id']]['accept_commands'] = self.str2bool(str(request.args['cmd']))
  894.             self.chats[request.args['id']]['send_notifications'] = self.str2bool(str(request.args['note']))
  895.             self.chats[request.args['id']]['allow_users'] = self.str2bool(str(request.args['allow']))
  896.             self._logger.debug("Updated chat - " + str(request.args['id']))
  897.         elif 'bindings' in request.args:
  898.             bind_text = {}
  899.             for key in {k: v for k, v in telegramMsgDict.iteritems() if 'bind_msg' in v }:
  900.                 if telegramMsgDict[key]['bind_msg'] in bind_text:
  901.                     bind_text[telegramMsgDict[key]['bind_msg']].append(key)
  902.                 else:
  903.                     bind_text[telegramMsgDict[key]['bind_msg']] = [key]
  904.             return json.dumps({
  905.                 'bind_cmd':[k for k, v in self.tcmd.commandDict.iteritems() if 'bind_none' not in v ],
  906.                 'bind_msg':[k for k, v in telegramMsgDict.iteritems() if 'bind_msg' not in v ],
  907.                 'bind_text':bind_text,
  908.                 'no_setting':[k for k, v in telegramMsgDict.iteritems() if 'no_setting' in v ]})
  909.        
  910.         retChats = {k: v for k, v in self.chats.iteritems() if 'delMe' not in v and k != 'zBOTTOMOFCHATS'}
  911.         for chat in retChats:
  912.             if os.path.isfile(self.get_plugin_data_folder()+"/img/user/pic" +chat+".jpg"):
  913.                 retChats[chat]['image'] = "/plugin/telegram/img/user/pic" +chat+".jpg"
  914.             elif int(chat) < 0:
  915.                 retChats[chat]['image'] = "/plugin/telegram/img/static/group.jpg"
  916.             else:
  917.                 retChats[chat]['image'] = "/plugin/telegram/img/static/default.jpg"
  918.  
  919.         return json.dumps({'chats':retChats, 'connection_state_str':self.connection_state_str, 'connection_ok':self.connection_ok})
  920.    
  921.     def on_api_command(self, command, data):
  922.         if command=="testToken":
  923.             self._logger.debug("Testing token {}".format(data['token']))
  924.             try:
  925.                 if self._settings.get(["token"]) != data["token"]:
  926.                     username = self.test_token(data['token'])
  927.                     self._settings.set(['token'], data['token'])
  928.                     self.stop_listening() #to start with new token if already running
  929.                     self.start_listening()
  930.                     return json.dumps({'ok': True, 'connection_state_str': gettext("Token valid for %(username)s.", username=username), 'error_msg': None, 'username': username})
  931.                 return json.dumps({'ok': True, 'connection_state_str': gettext("Token valid for %(username)s.", username=self.thread.username), 'error_msg': None, 'username': self.thread.username})
  932.             except Exception as ex:
  933.                 return json.dumps({'ok': False, 'connection_state_str': gettext("Error: %(error)s", error=ex), 'username': None, 'error_msg': str(ex)})
  934.         # delete a chat (will not be removed and show up again on octorint restart
  935.         # if save button is not pressed on settings dialog)
  936.         elif command=="delChat":
  937.             strId = str(data['ID'])
  938.             if strId in self.chats:
  939.                 del self.chats[strId]
  940.                 # do self._settings.save() here???????
  941.             return json.dumps({'chats':{k: v for k, v in self.chats.iteritems() if 'delMe' not in v and k != 'zBOTTOMOFCHATS'}, 'connection_state_str':self.connection_state_str, 'connection_ok':self.connection_ok})
  942.  
  943. ##########
  944. ### Telegram API-Functions
  945. ##########
  946.  
  947.     def send_msg(self, message, **kwargs):
  948.         if not self.send_messages:
  949.             return
  950.            
  951.         kwargs['message'] = message
  952.         try:
  953.             # If it's a regular event notification
  954.             if 'chatID' not in kwargs and 'event' in kwargs:
  955.                 self._logger.debug("Send_msg() found event: " + str(kwargs['event']))
  956.                 for key in self.chats:
  957.                     if key != 'zBOTTOMOFCHATS':
  958.                         if self.chats[key]['notifications'][kwargs['event']] and (key not in self.shut_up or self.shut_up[key]==0) and self.chats[key]['send_notifications']:
  959.                             kwargs['chatID'] = key
  960.                             t = threading.Thread(target=self._send_msg, kwargs = kwargs).run()
  961.             # Seems to be a broadcast
  962.             elif 'chatID' not in kwargs:
  963.                 for key in self.chats:
  964.                     kwargs['chatID'] = key
  965.                     t = threading.Thread(target=self._send_msg, kwargs = kwargs).run()
  966.             # This is a 'editMessageText' message
  967.             elif 'msg_id' in kwargs and kwargs['msg_id'] is not "" and  kwargs['msg_id'] is not None:
  968.                 t = threading.Thread(target=self._send_edit_msg, kwargs = kwargs).run()
  969.             # direct message or event notification to a chat_id
  970.             else:
  971.                 t = threading.Thread(target=self._send_msg, kwargs = kwargs).run()
  972.         except Exception as ex:
  973.             self._logger.debug("Caught an exception in send_msg(): " + str(ex))
  974.  
  975.  
  976.     # this method is used to update a message text of a sent message
  977.     # the sent message had to have no_markup = true when calling send_msg() (otherwise it would not work)
  978.     # by setting no_markup = true we got a messageg_id on sending the message which is saved in selfupdateMessageID
  979.     # if this message_id is passed in msg_id to send_msg() then this method will be called
  980.     def _send_edit_msg(self,message="",msg_id="",chatID="", responses= None, inline=True, markup=None,delay=0, **kwargs):
  981.         if not self.send_messages:
  982.             return
  983.            
  984.         if delay > 0:
  985.             time.sleep(delay)
  986.         try:
  987.             self._logger.debug("Sending a message UPDATE: " + message.replace("\n", "\\n") + " chatID= " + str(chatID))
  988.             data = {}
  989.             data['text'] = message
  990.             data['message_id'] = msg_id
  991.             data['chat_id'] = int(chatID)
  992.             if markup is not None:
  993.                 if "HTML" in markup  or "Markdown" in markup:
  994.                     data["parse_mode"] = markup
  995.             if responses and inline:
  996.                 myArr = []
  997.                 for k in responses:
  998.                     myArr.append(map(lambda x: {"text":x[0],"callback_data":x[1]}, k))
  999.                 keyboard = {'inline_keyboard':myArr}
  1000.                 data['reply_markup'] = json.dumps(keyboard)
  1001.             self._logger.debug("SENDING UPDATE: " + str(data))
  1002.             req = requests.post(self.bot_url + "/editMessageText", data=data, proxies=self.getProxies())
  1003.             if req.headers['content-type'] != 'application/json':
  1004.                 self._logger.debug(gettext("Unexpected Content-Type. Expected: application/json. Was: %(type)s. Waiting 2 minutes before trying again.", type=req.headers['content-type']))
  1005.                 return
  1006.             myJson = req.json()
  1007.             self._logger.debug("REQUEST RES: "+str(myJson))
  1008.             if inline:
  1009.                 self.updateMessageID[chatID] = msg_id
  1010.         except Exception as ex:
  1011.             self._logger.debug("Caught an exception in _send_edit_msg(): " + str(ex))
  1012.  
  1013.     def _send_msg(self, message="", with_image=False, responses=None, delay=0, inline = True, chatID = "", markup=None, showWeb=False, **kwargs):
  1014.         if not self.send_messages:
  1015.             return
  1016.            
  1017.         if delay > 0:
  1018.             time.sleep(delay)
  1019.         try:
  1020.             if with_image:
  1021.                 if 'event' in kwargs and not self._settings.get(["messages",kwargs['event'],"combined"]):
  1022.                     args = locals()
  1023.                     del args['kwargs']['event']
  1024.                     del args['self']
  1025.                     args['message'] = ""
  1026.                     self._logger.debug("Sending image...")
  1027.                     t = threading.Thread(target=self._send_msg, kwargs = args).run()
  1028.                     args['message'] = message
  1029.                     args['with_image'] = False
  1030.                     self._logger.debug("Sending text...")
  1031.                     t = threading.Thread(target=self._send_msg, kwargs = args).run()
  1032.                     return
  1033.  
  1034.             self._logger.debug("Sending a message: " + message.replace("\n", "\\n") + " with_image=" + str(with_image) + " chatID= " + str(chatID))
  1035.             data = {}
  1036.             # Do we want to show web link previews?
  1037.             data['disable_web_page_preview'] = not showWeb  
  1038.             # Do we want the message to be parsed in any markup?
  1039.             if markup is not None:
  1040.                 if "HTML" in markup  or "Markdown" in markup:
  1041.                     data["parse_mode"] = markup
  1042.             if responses:
  1043.                 myArr = []
  1044.                 for k in responses:
  1045.                     myArr.append(map(lambda x: {"text":x[0],"callback_data":x[1]}, k))
  1046.                 keyboard = {'inline_keyboard':myArr}
  1047.                 data['reply_markup'] = json.dumps(keyboard)
  1048.                
  1049.             image_data = None
  1050.             if with_image:
  1051.                 image_data = self.take_image()
  1052.             self._logger.debug("data so far: " + str(data))
  1053.  
  1054.             if not image_data and with_image:
  1055.                 message = "[ERR GET IMAGE]\n\n" + message
  1056.  
  1057.             r = None
  1058.             data['chat_id'] = chatID
  1059.             if image_data:
  1060.                 self._logger.debug("Sending with image.. " + str(chatID))
  1061.                 files = {'photo':("image.jpg", image_data)}
  1062.                 if message is not "":
  1063.                     data['caption'] = message
  1064.                 r = requests.post(self.bot_url + "/sendPhoto", files=files, data=data, proxies=self.getProxies())
  1065.                 self._logger.debug("Sending finished. " + str(r.status_code))
  1066.             else:
  1067.                 self._logger.debug("Sending without image.. " + str(chatID))
  1068.                 data['text'] = message
  1069.                 r =requests.post(self.bot_url + "/sendMessage", data=data, proxies=self.getProxies())
  1070.                 self._logger.debug("Sending finished. " + str(r.status_code))
  1071.  
  1072.             if r is not None and inline:
  1073.                 r.raise_for_status()
  1074.                 myJson = r.json()
  1075.                 if not myJson['ok']:
  1076.                     raise NameError("ReqErr")
  1077.                 if 'message_id' in myJson['result']:
  1078.                     self.updateMessageID[chatID] = myJson['result']['message_id']
  1079.         except Exception as ex:
  1080.             self._logger.debug("Caught an exception in _send_msg(): " + str(ex))
  1081.    
  1082.     def send_file(self,chat_id,path):
  1083.         if not self.send_messages:
  1084.             return
  1085.            
  1086.         try:
  1087.             requests.get(self.bot_url + "/sendChatAction", params = {'chat_id': chat_id, 'action': 'upload_document'}, proxies=self.getProxies())
  1088.             files = {'document': open(path, 'rb')}
  1089.             r = requests.post(self.bot_url + "/sendDocument", files=files, data={'chat_id':chat_id}, proxies=self.getProxies())
  1090.         except Exception as ex:
  1091.             pass
  1092.  
  1093.     def send_video(self, message, video_file):
  1094.         if not self.send_messages:
  1095.             return
  1096.            
  1097.         files = {'video': open(video_file, 'rb')}
  1098.         #r = requests.post(self.bot_url + "/sendVideo", files=files, data={'chat_id':self._settings.get(["chat"]), 'caption':message})
  1099.         self._logger.debug("Sending finished. " + str(r.status_code) + " " + str(r.content))
  1100.    
  1101.     def get_file(self, file_id):
  1102.         if not self.send_messages:
  1103.             return
  1104.            
  1105.         self._logger.debug("Requesting file with id %s.", file_id)
  1106.         r = requests.get(self.bot_url + "/getFile", data={'file_id': file_id}, proxies=self.getProxies())
  1107.         # {"ok":true,"result":{"file_id":"BQADAgADCgADrWJxCW_eFdzxDPpQAg","file_size":26,"file_path":"document\/file_3.gcode"}}
  1108.         r.raise_for_status()
  1109.         data = r.json()
  1110.         if not "ok" in data:
  1111.             raise Exception(_("Telegram didn't respond well to getFile. The response was: %(response)s", response=r.text))
  1112.         url = self.bot_file_url + "/" + data['result']['file_path']
  1113.         self._logger.debug("Downloading file: %s", url)
  1114.         r = requests.get(url, proxies=self.getProxies())
  1115.         r.raise_for_status()
  1116.         return r.content
  1117.  
  1118.     def get_usrPic(self,chat_id, file_id=""):
  1119.         if not self.send_messages:
  1120.             return
  1121.            
  1122.         self._logger.debug("Requesting Profile Photo for chat_id: " + str(chat_id))
  1123.         try:
  1124.             if file_id == "":
  1125.                 if int(chat_id) < 0:
  1126.                     self._logger.debug("Not able to load group photos. "+ str(chat_id)+" EXIT")
  1127.                     return
  1128.                 r = requests.get(self.bot_url + "/getUserProfilePhotos", params = {'limit': 1, "user_id": chat_id}, proxies=self.getProxies())
  1129.                 r.raise_for_status()
  1130.                 data = r.json()
  1131.                 if not "ok" in data:
  1132.                     raise Exception(_("Telegram didn't respond well to getUserProfilePhoto "+ str(chat_id)+". The response was: %(response)s", response=r.text))
  1133.                 if data['result']['total_count'] < 1:
  1134.                     self._logger.debug("NO PHOTOS "+ str(chat_id)+". EXIT")
  1135.                     return
  1136.                 r = self.get_file(data['result']['photos'][0][0]['file_id'])
  1137.             else:
  1138.                 r = self.get_file(file_id)
  1139.             file_name = self.get_plugin_data_folder() + "/img/user/pic" + str(chat_id) + ".jpg"
  1140.             img = Image.open(StringIO.StringIO(r))
  1141.             img = img.resize((40, 40), PIL.Image.ANTIALIAS)
  1142.             img.save(file_name, format="JPEG")
  1143.             self._logger.debug("Saved Photo "+ str(chat_id))
  1144.  
  1145.         except Exception as ex:
  1146.             self._logger.error("Can't load UserImage: " + str(ex))
  1147.    
  1148.     def test_token(self, token=None):
  1149.         if not self.send_messages:
  1150.             return
  1151.            
  1152.         if token is None:
  1153.             token = self._settings.get(["token"])
  1154.         response = requests.get("https://api.telegram.org/bot" + token + "/getMe", proxies=self.getProxies())
  1155.         self._logger.debug("getMe returned: " + str(response.json()))
  1156.         self._logger.debug("getMe status code: " + str(response.status_code))
  1157.         json = response.json()
  1158.         if not 'ok' in json or not json['ok']:
  1159.             if json['description']:
  1160.                 raise(Exception(gettext("Telegram returned error code %(error)s: %(message)s", error=json['error_code'], message=json['description'])))
  1161.             else:
  1162.                 raise(Exception(gettext("Telegram returned an unspecified error.")))
  1163.         else:
  1164.             return "@" + json['result']['username']
  1165.  
  1166. ##########
  1167. ### Helper methods
  1168. ##########
  1169.  
  1170.     def str2bool(self,v):
  1171.         return v.lower() in ("yes", "true", "t", "1")
  1172.  
  1173.     def set_log_level(self):
  1174.         self._logger.setLevel(logging.DEBUG if self._settings.get_boolean(["debug"]) else logging.NOTSET)
  1175.  
  1176.  
  1177.     def getProxies(self):
  1178.         http_proxy = self._settings.get(["http_proxy"])
  1179.         https_proxy = self._settings.get(["https_proxy"])
  1180.         return {
  1181.             'http': http_proxy,
  1182.             'https': https_proxy
  1183.             }
  1184.  
  1185. # checks if the received command is allowed to execute by the user
  1186.     def isCommandAllowed(self, chat_id, from_id, command):
  1187.         if 'bind_none' in self.tcmd.commandDict[command]:
  1188.             return True
  1189.         if command is not None or command is not "":
  1190.             if self.chats[chat_id]['accept_commands']:
  1191.                 if self.chats[chat_id]['commands'][command]:
  1192.                         return True
  1193.                 elif int(chat_id) < 0 and self.chats[chat_id]['allow_users']:
  1194.                     if self.chats[from_id]['commands'][command] and self.chats[from_id]['accept_commands']:
  1195.                         return True
  1196.             elif int(chat_id) < 0 and self.chats[chat_id]['allow_users']:
  1197.                 if self.chats[from_id]['commands'][command] and self.chats[from_id]['accept_commands']:
  1198.                         return True
  1199.         return False
  1200.  
  1201.     # Helper function to handle /editMessageText Telegram API commands
  1202.     # see main._send_edit_msg()
  1203.     def getUpdateMsgId(self,id):
  1204.         uMsgID = ""
  1205.         if id in self.updateMessageID:
  1206.             uMsgID = self.updateMessageID[id]
  1207.             del self.updateMessageID[id]
  1208.         return uMsgID
  1209.  
  1210.     def take_image(self):
  1211.         snapshot_url = self._settings.global_get(["webcam", "snapshot"])
  1212.         self._logger.debug("Snapshot URL: " + str(snapshot_url))
  1213.         data = None
  1214.         if snapshot_url:
  1215.             try:
  1216.                 r = requests.get(snapshot_url)
  1217.                 data = r.content
  1218.             except Exception as e:
  1219.                 return None
  1220.         flipH = self._settings.global_get(["webcam", "flipH"])
  1221.         flipV = self._settings.global_get(["webcam", "flipV"])
  1222.         rotate= self._settings.global_get(["webcam", "rotate90"])
  1223.        
  1224.         if flipH or flipV or rotate:
  1225.             image = Image.open(StringIO.StringIO(data))
  1226.             if flipH:
  1227.                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
  1228.             if flipV:
  1229.                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
  1230.             if rotate:
  1231.                 image = image.transpose(Image.ROTATE_90)
  1232.             output = StringIO.StringIO()
  1233.             image.save(output, format="JPEG")
  1234.             data = output.getvalue()
  1235.             output.close()
  1236.         return data
  1237.        
  1238.     def track_action(self, action):
  1239.         if not self._settings.get_boolean(["tracking_activated"]):
  1240.             return
  1241.         if self._settings.get(["tracking_token"]) is None:
  1242.             token = "".join(random.choice("abcdef0123456789") for i in xrange(16))
  1243.             self._settings.set(["tracking_token"], token)
  1244.         params = {
  1245.             'idsite': '3',
  1246.             'rec': '1',
  1247.             'url': 'http://octoprint-telegram/'+action,
  1248.             'action_name': ("%20/%20".join(action.split("/"))),
  1249.             '_id': self._settings.get(["tracking_token"]),
  1250.             'uid': self._settings.get(["tracking_token"]),
  1251.             'cid': self._settings.get(["tracking_token"]),
  1252.             'send_image': '0',
  1253.             '_idvc': '1',
  1254.             'dimension1': str(self._plugin_version)
  1255.         }
  1256.         t = threading.Thread(target=requests.get, args=("http://piwik.schlenz.ruhr/piwik.php",), kwargs={'params': params})
  1257.         t.daemon = True
  1258.         t.run()
  1259.  
  1260.     def route_hook(self, server_routes, *args, **kwargs):
  1261.         from octoprint.server.util.tornado import LargeResponseHandler, UrlProxyHandler, path_validation_factory
  1262.         from octoprint.util import is_hidden_path
  1263.         if not os.path.exists(self.get_plugin_data_folder()+"/img"):
  1264.             os.mkdir(self.get_plugin_data_folder()+"/img")
  1265.         if not os.path.exists(self.get_plugin_data_folder()+"/img/user"):
  1266.             os.mkdir(self.get_plugin_data_folder()+"/img/user")
  1267.  
  1268.         return [
  1269.                 (r"/img/user/(.*)", LargeResponseHandler, dict(path=self.get_plugin_data_folder() + r"/img/user/", as_attachment=True,allow_client_caching =False)),
  1270.                 (r"/img/static/(.*)", LargeResponseHandler, dict(path=self._basefolder + "/static/img/", as_attachment=True,allow_client_caching =True))
  1271.                 ]
  1272.  
  1273. ########################################
  1274. ########################################
  1275. ### Some methods to check version and
  1276. ### get the right implementation
  1277. ########################################
  1278. ########################################
  1279.  
  1280. # copied from pluginmanager plugin
  1281. def _is_octoprint_compatible(compatibility_entries):
  1282.     """
  1283.     Tests if the current octoprint_version is compatible to any of the provided ``compatibility_entries``.
  1284.     """
  1285.  
  1286.     octoprint_version = _get_octoprint_version()
  1287.     for octo_compat in compatibility_entries:
  1288.         if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
  1289.             octo_compat = ">={}".format(octo_compat)
  1290.  
  1291.         s = next(pkg_resources.parse_requirements("OctoPrint" + octo_compat))
  1292.         if octoprint_version in s:
  1293.             break
  1294.     else:
  1295.         return False
  1296.  
  1297.     return True
  1298.  
  1299. # copied from pluginmanager plugin
  1300. def _get_octoprint_version():
  1301.     from octoprint.server import VERSION
  1302.     octoprint_version_string = VERSION
  1303.  
  1304.     if "-" in octoprint_version_string:
  1305.         octoprint_version_string = octoprint_version_string[:octoprint_version_string.find("-")]
  1306.  
  1307.     octoprint_version = pkg_resources.parse_version(octoprint_version_string)
  1308.     if isinstance(octoprint_version, tuple):
  1309.         # old setuptools
  1310.         base_version = []
  1311.         for part in octoprint_version:
  1312.             if part.startswith("*"):
  1313.                 break
  1314.             base_version.append(part)
  1315.         octoprint_version = ".".join(base_version)
  1316.     else:
  1317.         # new setuptools
  1318.         octoprint_version = pkg_resources.parse_version(octoprint_version.base_version)
  1319.  
  1320.     return octoprint_version
  1321. # check if we have min version 1.3.0
  1322. # this is important because of WizardPlugin mixin and folders in filebrowser
  1323. def get_implementation_class():
  1324.     if not _is_octoprint_compatible(["1.3.0"]):
  1325.         return TelegramPlugin(1.2)
  1326.     else:
  1327.         class NewTelegramPlugin(TelegramPlugin,octoprint.plugin.WizardPlugin):
  1328.             def __init__(self,version):
  1329.                 super(self.__class__, self).__init__(version)
  1330.         return NewTelegramPlugin(1.3)
  1331.  
  1332.        
  1333. __plugin_name__ = "Telegram Notifications"
  1334. __plugin_implementation__ = get_implementation_class()
  1335. __plugin_hooks__ = {
  1336.     "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
  1337.     "octoprint.server.http.routes": __plugin_implementation__.route_hook
  1338. }
Add Comment
Please, Sign In to add comment