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