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()):