From ad21146c6033237dc3413acdc781e5a1f3069c8b Mon Sep 17 00:00:00 2001 From: sirdel <3763715+sirdel@users.noreply.github.com> Date: Wed, 20 Apr 2022 13:05:25 +0200 Subject: [PATCH 1/2] Send telegram bug fix (#2420) Fixed bug with telegram settings not being stored correctly within the config file, removed msg_id argument, remove old email code, iterate through futures result. --- motioneye/config.py | 9 ++--- motioneye/extra/motioneye.conf.sample | 3 ++ motioneye/sendtelegram.py | 54 +++++++-------------------- 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/motioneye/config.py b/motioneye/config.py index 05aba4967..32facf81f 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -1158,8 +1158,7 @@ def motion_camera_ui_to_dict(ui, prev_config=None): on_event_start.append(line) if ui['telegram_notifications_enabled']: line = ( - "%(script)s '%(api)s' '%(chatid)s' " - "'motion_start' '%%t' '%%Y-%%m-%%dT%%H:%%M:%%S' '%(timespan)s'" + "%(script)s '%(api)s' '%(chatid)s' '%%t' '%%Y-%%m-%%dT%%H:%%M:%%S' '%(timespan)s'" % { 'script': meyectl.find_command('sendtelegram'), 'api': ui['telegram_notifications_api'], @@ -1669,12 +1668,12 @@ def motion_camera_dict_to_ui(data): elif ' sendtelegram ' in e: e = shlex.split(e) - if len(e) < 7: + if len(e) < 6: continue ui['telegram_notifications_enabled'] = True - ui['telegram_notifications_api'] = e[-6] - ui['telegram_notifications_chat_id'] = e[-5] + ui['telegram_notifications_api'] = e[-5] + ui['telegram_notifications_chat_id'] = e[-4] try: ui['telegram_notifications_picture_time_span'] = int(e[-1]) diff --git a/motioneye/extra/motioneye.conf.sample b/motioneye/extra/motioneye.conf.sample index d462b7ee9..b5505819f 100644 --- a/motioneye/extra/motioneye.conf.sample +++ b/motioneye/extra/motioneye.conf.sample @@ -80,6 +80,9 @@ list_media_timeout 120 # timeout in seconds to wait for media files list, when sending emails list_media_timeout_email 10 +# timeout in seconds to wait for media files list, when sending a telegram +list_media_timeout_telegram 10 + # timeout in seconds to wait for zip file creation zip_timeout 500 diff --git a/motioneye/sendtelegram.py b/motioneye/sendtelegram.py index f8994c652..52f054c6f 100644 --- a/motioneye/sendtelegram.py +++ b/motioneye/sendtelegram.py @@ -15,27 +15,19 @@ # along with this program. If not, see . import datetime -import io import logging import os import re import signal import socket -import sys import time import pycurl from tornado.ioloop import IOLoop -from motioneye import config, mediafiles, motionctl, settings, utils +from motioneye import config, mediafiles, meyectl, motionctl, settings, utils from motioneye.controls import tzctl -messages = { - 'motion_start': 'Motion has been detected by camera "%(camera)s/%(hostname)s" at %(moment)s (%(timezone)s).' -} - -user_agent = 'motionEye' - def send_message(api_key, chat_id, message, files): telegram_message_url = 'https://api.telegram.org/bot%s/sendMessage' % api_key @@ -62,7 +54,7 @@ def send_message(api_key, chat_id, message, files): ) c.perform() c.close() - logging.debug('sending email message') + logging.debug('sending telegram') def make_message(message, camera_id, moment, timespan, callback): @@ -80,7 +72,7 @@ def on_media_files(media_files): logging.debug('got media files') media_files = [ m - for m in media_files + for m in media_files.result() if abs(m['timestamp'] - timestamp) < float(timespan) ] media_files.sort(key=lambda m: m['timestamp'], reverse=True) @@ -98,11 +90,10 @@ def on_media_files(media_files): if settings.LOCAL_TIME_FILE: format_dict['timezone'] = tzctl.get_time_zone() - else: format_dict['timezone'] = 'local time' - logging.debug('creating email message') + logging.debug('creating telegram message') m = message % format_dict @@ -111,7 +102,7 @@ def on_media_files(media_files): if not timespan: return on_media_files([]) - logging.debug('waiting for pictures to be taken') + logging.debug(f'waiting {float(timespan)}s for pictures to be taken') time.sleep(float(timespan)) # give motion some time to create motion pictures prefix = None @@ -125,55 +116,41 @@ def on_media_files(media_files): and not snapshot_filename or snapshot_filename.startswith('%Y-%m-%d/') ): - moment = datetime.datetime.strptime(moment, '%Y-%m-%dT%H:%M:%S') prefix = moment.strftime('%Y-%m-%d') logging.debug('narrowing down still images path lookup to %s' % prefix) - ## mediafiles.list_media(camera_config, media_type='picture', prefix=prefix, callback=on_media_files) fut = utils.cast_future( mediafiles.list_media(camera_config, media_type='picture', prefix=prefix) ) fut.add_done_callback(on_media_files) - io_loop.start() def parse_options(parser, args): + parser.description = 'Send Telegram using bot api' parser.add_argument('api', help='telegram api key') parser.add_argument('chatid', help='telegram chat room id') - parser.add_argument('msg_id', help='the identifier of the message') parser.add_argument('motion_camera_id', help='the id of the motion camera') - parser.add_argument('moment', help='the moment in ISO-8601 format') + parser.add_argument( + 'moment', + help='the moment in ISO-8601 format', + type=datetime.datetime.fromisoformat, + ) parser.add_argument('timespan', help='picture collection time span') return parser.parse_args(args) def main(parser, args): - import meyectl # the motion daemon overrides SIGCHLD, # so we must restore it here, # or otherwise media listing won't work signal.signal(signal.SIGCHLD, signal.SIG_DFL) - if len(args) == 12: - # backwards compatibility with older configs lacking "from" field - _from = 'motionEye on {} <{}>'.format( - socket.gethostname(), args[7].split(',')[0] - ) - args = args[:7] + [_from] + args[7:] - - if not args[7]: - args[7] = 'motionEye on {} <{}>'.format( - socket.gethostname(), args[8].split(',')[0] - ) - options = parse_options(parser, args) - print(options) meyectl.configure_logging('telegram', options.log_to_file) - - logging.debug('hello!') - message = messages.get(options.msg_id) + logging.debug(options) + message = 'Motion has been detected by camera "%(camera)s/%(hostname)s" at %(moment)s (%(timezone)s).' # do not wait too long for media list, # telegram notifications are critical @@ -181,12 +158,9 @@ def main(parser, args): camera_id = motionctl.motion_camera_id_to_camera_id(options.motion_camera_id) - logging.debug('timespan = %d' % int(options.timespan)) - def on_message(message, files): try: - print(message) - logging.info('sending telegram') + logging.info(f'sending telegram : {message}') send_message(options.api, options.chatid, message, files or []) logging.info('telegram sent') From a5ed1c6a805dbc0dc7d60a4f0324b2971c591cc5 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Wed, 20 Apr 2022 19:26:07 +0200 Subject: [PATCH 2/2] Fix and enhance request handling (#2416) Bytes-encode all URL-encoded request key-value data. "urlencode" produces a string, but "urlopen" requires bytes, at least for POST requests: https://github.com/motioneye-project/motioneye/issues/2408 Import only used methods and classes from urllib. Consequently use f-strings instead of %-formatting. Signed-off-by: MichaIng --- motioneye/uploadservices.py | 110 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/motioneye/uploadservices.py b/motioneye/uploadservices.py index 8ad6eddec..5669afb8b 100644 --- a/motioneye/uploadservices.py +++ b/motioneye/uploadservices.py @@ -23,9 +23,9 @@ import os import os.path import time -import urllib.error -import urllib.parse -import urllib.request +from urllib.error import HTTPError +from urllib.parse import quote, urlencode +from urllib.request import Request import boto3 import pycurl @@ -106,7 +106,7 @@ def upload_file(self, target_dir, filename, camera_name): self.upload_data(rel_filename, mime_type, data, ctime, camera_name) - self.debug('file "%s" successfully uploaded' % filename) + self.debug(f'file "{filename}" successfully uploaded') def upload_data(self, filename, mime_type, data, ctime, camera_name): pass @@ -173,7 +173,7 @@ def _get_authorize_url(cls): 'access_type': 'offline', } - return cls.AUTH_URL + '?' + urllib.parse.urlencode(query) + return cls.AUTH_URL + '?' + urlencode(query) def _test_access(self): try: @@ -214,20 +214,20 @@ def _request(self, url, body=None, headers=None, retry_auth=True, method=None): self.save() except Exception as e: - self.error('failed to obtain credentials: %s' % e) + self.error(f'failed to obtain credentials: {e}') raise headers = headers or {} - headers['Authorization'] = 'Bearer %s' % self._credentials['access_token'] + headers['Authorization'] = f'Bearer {self._credentials["access_token"]}' - self.debug('requesting %s' % url) - request = urllib.request.Request(url, data=body, headers=headers) + self.debug(f'requesting {url}') + request = Request(url, data=body, headers=headers) if method: request.get_method = lambda: method try: response = utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: if ( e.code == 401 and retry_auth ): # unauthorized, access token may have expired @@ -253,11 +253,11 @@ def _request(self, url, body=None, headers=None, retry_auth=True, method=None): except Exception: msg = str(e) - self.error('request failed: %s' % msg) + self.error(f'request failed: {msg}') raise Exception(msg) except Exception as e: - self.error('request failed: %s' % e) + self.error(f'request failed: {e}') raise return response.read() @@ -283,14 +283,14 @@ def _request_credentials(self, authorization_key): 'scope': self.SCOPE, 'grant_type': 'authorization_code', } - body = urllib.parse.urlencode(body) + body = urlencode(body).encode() - request = urllib.request.Request(self.TOKEN_URL, data=body, headers=headers) + request = Request(self.TOKEN_URL, data=body, headers=headers) try: response = utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: error = json.load(e) raise Exception( error.get('error_description') or error.get('error') or str(e) @@ -312,14 +312,14 @@ def _refresh_credentials(self, refresh_token): 'client_secret': self.CLIENT_NOT_SO_SECRET, 'grant_type': 'refresh_token', } - body = urllib.parse.urlencode(body) + body = urlencode(body).encode() - request = urllib.request.Request(self.TOKEN_URL, data=body, headers=headers) + request = Request(self.TOKEN_URL, data=body, headers=headers) try: response = utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: error = json.load(e) raise Exception( error.get('error_description') or error.get('error') or str(e) @@ -375,17 +375,17 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): json.dumps(metadata), '', '--' + self.BOUNDARY, - 'Content-Type: %s' % mime_type, + f'Content-Type: {mime_type}', '', '', ] body = '\r\n'.join(body) body += data - body += '\r\n--%s--' % self.BOUNDARY + body += f'\r\n--{self.BOUNDARY}--' headers = { - 'Content-Type': 'multipart/related; boundary="%s"' % self.BOUNDARY, + 'Content-Type': f'multipart/related; boundary="{self.BOUNDARY}"', 'Content-Length': len(body), } @@ -410,7 +410,7 @@ def _get_folder_id(self, path=''): location += path if not folder_id or (now - folder_id_time > self.FOLDER_ID_LIFE_TIME): - self.debug('finding folder id for location "%s"' % location) + self.debug(f'finding folder id for location "{location}"') folder_id = self._get_folder_id_by_path(location) self._folder_ids[path] = folder_id @@ -436,7 +436,7 @@ def _get_folder_id_by_name(self, parent_id, child_name, create=True): 'parent_id': parent_id, 'child_name': child_name, } - query = urllib.parse.quote(query) + query = quote(query) else: query = '' @@ -461,13 +461,13 @@ def _get_folder_id_by_name(self, parent_id, child_name, create=True): if not items: if create: self.debug( - 'folder with name "%s" does not exist, creating it' % child_name + f'folder with name "{child_name}" does not exist, creating it' ) self._create_folder(parent_id, child_name) return self._get_folder_id_by_name(parent_id, child_name, create=False) else: - msg = 'folder with name "%s" does not exist' % child_name + msg = f'folder with name "{child_name}" does not exist' self.error(msg) raise Exception(msg) @@ -496,17 +496,17 @@ def clean_cloud(self, cloud_dir, local_folders): self.info( f'found {len(local_folders)}/{len(children)} folder(s) in local/cloud' ) - self.debug('local %s' % local_folders) + self.debug(f'local {local_folders}') for child in children: id = child['id'] name = self._get_file_title(id) - self.debug("cloud '%s'" % name) + self.debug(f"cloud '{name}'") to_delete = not exist_in_local(name, local_folders) if to_delete and self._delete_file(id): removed_count += 1 - self.info("deleted a cloud folder '%s'" % name) + self.info(f"deleted a cloud folder '{name}'") - self.info('deleted %s cloud folder(s)' % removed_count) + self.info(f'deleted {removed_count} cloud folder(s)') return removed_count def _get_children(self, file_id): @@ -579,7 +579,7 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): uploadToken = self._request(self.GOOGLE_PHOTO_API + 'uploads', body, headers) response = self._create_media(uploadToken, camera_name) - self.debug('response %s' % response['mediaItem']) + self.debug(f'response {response["mediaItem"]}') def dump(self): return self._dump() @@ -595,7 +595,7 @@ def _get_folder_id(self, path=''): self.debug(f'_get_folder_id({path}, {location}, {folder_id})') if not folder_id: - self.debug('finding album with title "%s"' % location) + self.debug(f'finding album with title "{location}"') folder_id = self._get_folder_id_by_name(location) self._folder_ids[location] = folder_id @@ -623,7 +623,7 @@ def _get_folder_id_by_name(self, name, create=True): return albumId except Exception as e: - self.error("_get_folder_id_by_name() failed: %s" % e) + self.error(f"_get_folder_id_by_name() failed: {e}") raise def _create_folder(self, parent_id, child_name): @@ -638,7 +638,7 @@ def _create_folder(self, parent_id, child_name): def _create_media(self, uploadToken, camera_name): description = 'captured by motionEye camera' + ( - ' "%s"' % camera_name if camera_name else '' + f' "{camera_name}"' if camera_name else '' ) metadata = { @@ -664,7 +664,7 @@ def _get_albums(self): response = self._request_json(self.GOOGLE_PHOTO_API + 'albums') albums = response.get('albums') - self.debug('got %s album(s)' % len(albums)) + self.debug(f'got {len(albums)} album(s)') return albums def _filter_albums(self, albums, title): @@ -698,7 +698,7 @@ def get_authorize_url(cls): 'token_access_type': 'offline', } - return cls.AUTH_URL + '?' + urllib.parse.urlencode(query) + return cls.AUTH_URL + '?' + urlencode(query) def test_access(self): body = { @@ -778,18 +778,18 @@ def _request(self, url, body=None, headers=None, retry_auth=True): self.save() except Exception as e: - self.error('failed to obtain credentials: %s' % e) + self.error(f'failed to obtain credentials: {e}') raise headers = headers or {} - headers['Authorization'] = 'Bearer %s' % self._credentials['access_token'] + headers['Authorization'] = f'Bearer {self._credentials["access_token"]}' - self.debug('requesting %s' % url) - request = urllib.request.Request(url, data=body, headers=headers) + self.debug(f'requesting {url}') + request = Request(url, data=body, headers=headers) try: response = utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: if ( e.code == 401 and retry_auth ): # unauthorized, access token may have expired @@ -808,16 +808,16 @@ def _request(self, url, body=None, headers=None, retry_auth=True): raise elif str(e).count('not_found'): - msg = 'folder "%s" not found' % self._location + msg = f'folder "{self._location}" not found' self.error(msg) raise Exception(msg) else: - self.error('request failed: %s' % e) + self.error(f'request failed: {e}') raise except Exception as e: - self.error('request failed: %s' % e) + self.error(f'request failed: {e}') raise return response.read() @@ -831,14 +831,14 @@ def _request_credentials(self, authorization_key): 'client_secret': self.CLIENT_NOT_SO_SECRET, 'grant_type': 'authorization_code', } - body = urllib.parse.urlencode(body) + body = urlencode(body).encode() - request = urllib.request.Request(self.TOKEN_URL, data=body, headers=headers) + request = Request(self.TOKEN_URL, data=body, headers=headers) try: response = utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: error = json.load(e) raise Exception( error.get('error_description') or error.get('error') or str(e) @@ -860,14 +860,14 @@ def _refresh_credentials(self, refresh_token): 'client_secret': self.CLIENT_NOT_SO_SECRET, 'grant_type': 'refresh_token', } - body = urllib.urlencode(body) + body = urlencode(body).encode() - request = urllib.request.Request(self.TOKEN_URL, data=body, headers=headers) + request = Request(self.TOKEN_URL, data=body, headers=headers) try: response = utils.urlopen(request) - except urllib.error.HTTPError as e: + except HTTPError as e: error = json.load(e) raise Exception( error.get('error_description') or error.get('error') or str(e) @@ -925,7 +925,7 @@ def upload_data(self, filename, mime_type, data, ctime, camera_name): conn.cwd(path) self.debug(f'uploading {filename} of {len(data)} bytes') - conn.storbinary('STOR %s' % filename, io.StringIO(data)) + conn.storbinary(f'STOR {filename}', io.StringIO(data)) self.debug('upload done') @@ -1035,7 +1035,7 @@ def test_access(self): return True except Exception as e: - logging.error('sftp connection failed: %s' % e) + logging.error(f'sftp connection failed: {e}') return str(e) @@ -1203,12 +1203,12 @@ def get(camera_id, service_name): def test_access(camera_id, service_name, data): - logging.debug('testing access to %s' % service_name) + logging.debug(f'testing access to {service_name}') service = get(camera_id, service_name) service.load(data) if not service: - return 'unknown upload service %s' % service_name + return f'unknown upload service {service_name}' return service.test_access() @@ -1242,7 +1242,7 @@ def _load(): file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME) if os.path.exists(file_path): - logging.debug('loading upload services state from "%s"...' % file_path) + logging.debug(f'loading upload services state from "{file_path}"...') try: f = open(file_path) @@ -1288,7 +1288,7 @@ def _load(): def _save(services): file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME) - logging.debug('saving upload services state to "%s"...' % file_path) + logging.debug(f'saving upload services state to "{file_path}"...') data = {} for camera_id, camera_services in list(services.items()):