Daniel Brötzmann pushed to branch mainwindow at gajim / gajim

Commits:

2 changed files:

Changes:

  • gajim/common/preview.py
    ... ... @@ -12,17 +12,26 @@
    12 12
     # You should have received a copy of the GNU General Public License
    
    13 13
     # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14 14
     
    
    15
    +from typing import Any
    
    16
    +from typing import Dict
    
    17
    +from typing import Optional
    
    18
    +from typing import Tuple
    
    19
    +
    
    15 20
     import logging
    
    16 21
     import os
    
    17 22
     from pathlib import Path
    
    18 23
     from urllib.parse import urlparse
    
    24
    +from urllib.parse import ParseResult
    
    19 25
     
    
    26
    +from gi.repository import GdkPixbuf
    
    27
    +from gi.repository import Gio
    
    20 28
     from gi.repository import GLib
    
    21 29
     from gi.repository import Soup
    
    22 30
     
    
    23 31
     from gajim.common import app
    
    24 32
     from gajim.common import configpaths
    
    25 33
     from gajim.common.const import MIME_TYPES
    
    34
    +from gajim.common.helpers import AdditionalDataDict
    
    26 35
     from gajim.common.helpers import load_file_async
    
    27 36
     from gajim.common.helpers import write_file_async
    
    28 37
     from gajim.common.helpers import get_tls_error_phrase
    
    ... ... @@ -46,7 +55,13 @@
    46 55
     
    
    47 56
     
    
    48 57
     class Preview:
    
    49
    -    def __init__(self, uri, urlparts, orig_path, thumb_path, size, widget):
    
    58
    +    def __init__(self, 
    
    59
    +                 uri: str,
    
    60
    +                 urlparts: Optional[ParseResult],
    
    61
    +                 orig_path: Optional[Path],
    
    62
    +                 thumb_path: Optional[Path],
    
    63
    +                 size: int,
    
    64
    +                 widget: Any) -> None:
    
    50 65
             self._uri = uri
    
    51 66
             self._urlparts = urlparts
    
    52 67
             self._filename = filename_from_uri(uri)
    
    ... ... @@ -57,41 +72,44 @@ def __init__(self, uri, urlparts, orig_path, thumb_path, size, widget):
    57 72
             self.thumb_path = thumb_path
    
    58 73
             self.size = size
    
    59 74
     
    
    60
    -        self.thumbnail = None
    
    61
    -        self.mime_type = None
    
    62
    -        self.file_size = 0
    
    75
    +        self.thumbnail: Optional[bytes] = None
    
    76
    +        self.mime_type: str = ''
    
    77
    +        self.file_size: int = 0
    
    63 78
     
    
    64
    -        self.key, self.iv = None, None
    
    65
    -        if self.is_aes_encrypted:
    
    79
    +        self.key: Optional[bytes] = None
    
    80
    +        self.iv: Optional[bytes] = None
    
    81
    +        if self.is_aes_encrypted and urlparts is not None:
    
    66 82
                 self.key, self.iv = parse_fragment(urlparts.fragment)
    
    67 83
     
    
    68 84
         @property
    
    69
    -    def is_geo_uri(self):
    
    85
    +    def is_geo_uri(self) -> bool:
    
    70 86
             return self._uri.startswith('geo:')
    
    71 87
     
    
    72 88
         @property
    
    73
    -    def is_web_uri(self):
    
    89
    +    def is_web_uri(self) -> bool:
    
    74 90
             return not self.is_geo_uri
    
    75 91
     
    
    76 92
         @property
    
    77
    -    def is_previewable(self):
    
    93
    +    def is_previewable(self) -> bool:
    
    78 94
             return self.mime_type in PREVIEWABLE_MIME_TYPES
    
    79 95
     
    
    80 96
         @property
    
    81
    -    def is_audio(self):
    
    97
    +    def is_audio(self) -> bool:
    
    82 98
             is_allowed = bool(self.mime_type in ALLOWED_MIME_TYPES)
    
    83 99
             return is_allowed and self.mime_type.startswith('audio/')
    
    84 100
     
    
    85 101
         @property
    
    86
    -    def uri(self):
    
    102
    +    def uri(self) -> str:
    
    87 103
             return self._uri
    
    88 104
     
    
    89 105
         @property
    
    90
    -    def filename(self):
    
    106
    +    def filename(self) -> str:
    
    91 107
             return self._filename
    
    92 108
     
    
    93 109
         @property
    
    94
    -    def request_uri(self):
    
    110
    +    def request_uri(self) -> Optional[str]:
    
    111
    +        if self._urlparts is None:
    
    112
    +            return ''
    
    95 113
             if self.is_aes_encrypted:
    
    96 114
                 # Remove fragments so we dont transmit it to the server
    
    97 115
                 urlparts = self._urlparts._replace(scheme='https', fragment='')
    
    ... ... @@ -99,31 +117,37 @@ def request_uri(self):
    99 117
             return self._urlparts.geturl()
    
    100 118
     
    
    101 119
         @property
    
    102
    -    def is_aes_encrypted(self):
    
    120
    +    def is_aes_encrypted(self) -> bool:
    
    103 121
             if self._urlparts is None:
    
    104 122
                 return False
    
    105 123
             return self._urlparts.scheme == 'aesgcm'
    
    106 124
     
    
    107
    -    def thumb_exists(self):
    
    125
    +    def thumb_exists(self) -> bool:
    
    126
    +        if self.thumb_path is None:
    
    127
    +            return False
    
    108 128
             return self.thumb_path.exists()
    
    109 129
     
    
    110
    -    def orig_exists(self):
    
    130
    +    def orig_exists(self) -> bool:
    
    131
    +        if self.orig_path is None:
    
    132
    +            return False
    
    111 133
             return self.orig_path.exists()
    
    112 134
     
    
    113
    -    def create_thumbnail(self, data):
    
    135
    +    def create_thumbnail(self, data: bytes) -> bool:
    
    114 136
             self.thumbnail = create_thumbnail(data, self.size)
    
    115 137
             if self.thumbnail is None:
    
    116 138
                 log.warning('Creating thumbnail failed for: %s', self.orig_path)
    
    117 139
                 return False
    
    118 140
             return True
    
    119 141
     
    
    120
    -    def update_widget(self, data=None):
    
    142
    +    def update_widget(self, data: Optional[GdkPixbuf.Pixbuf] = None) -> None:
    
    121 143
             self._widget.update(self, data)
    
    122 144
     
    
    123 145
     
    
    124 146
     class PreviewManager:
    
    125
    -    def __init__(self):
    
    126
    -        self._sessions = {}
    
    147
    +    def __init__(self) -> None:
    
    148
    +        self._sessions: Dict[
    
    149
    +            str,
    
    150
    +            Tuple[Soup.Session, Optional[Gio.SimpleProxyResolver]]] = {}
    
    127 151
     
    
    128 152
             self._orig_dir = Path(configpaths.get('MY_DATA')) / 'downloads'
    
    129 153
             self._thumb_dir = Path(configpaths.get('MY_CACHE')) / 'downloads.thumb'
    
    ... ... @@ -134,13 +158,14 @@ def __init__(self):
    134 158
             if GLib.mkdir_with_parents(str(self._thumb_dir), 0o700) != 0:
    
    135 159
                 log.error('Failed to create: %s', self._thumb_dir)
    
    136 160
     
    
    137
    -    def _get_session(self, account):
    
    161
    +    def _get_session(self, account: str) -> Soup.Session:
    
    138 162
             if account not in self._sessions:
    
    139 163
                 self._sessions[account] = self._create_session(account)
    
    140 164
             return self._sessions[account][0]
    
    141 165
     
    
    142 166
         @staticmethod
    
    143
    -    def _create_session(account):
    
    167
    +    def _create_session(account: str) -> Tuple[
    
    168
    +            Soup.Session, Optional[Gio.SimpleProxyResolver]]:
    
    144 169
             session = Soup.Session()
    
    145 170
             session.add_feature_by_type(Soup.ContentSniffer)
    
    146 171
             session.props.https_aliases = ['aesgcm']
    
    ... ... @@ -156,7 +181,9 @@ def _create_session(account):
    156 181
             return session, resolver
    
    157 182
     
    
    158 183
         @staticmethod
    
    159
    -    def _accept_uri(urlparts, uri, additional_data):
    
    184
    +    def _accept_uri(urlparts: ParseResult,
    
    185
    +                    uri: str,
    
    186
    +                    additional_data: AdditionalDataDict) -> bool:
    
    160 187
             try:
    
    161 188
                 oob_url = additional_data['gajim']['oob_url']
    
    162 189
             except (KeyError, AttributeError):
    
    ... ... @@ -190,7 +217,9 @@ def _accept_uri(urlparts, uri, additional_data):
    190 217
             log.info('Unsupported URI scheme: %s', uri)
    
    191 218
             return False
    
    192 219
     
    
    193
    -    def get_previewable(self, text, additional_data):
    
    220
    +    def get_previewable(self, 
    
    221
    +                        text: str, 
    
    222
    +                        additional_data: AdditionalDataDict) -> bool:
    
    194 223
             if len(text.split(' ')) > 1:
    
    195 224
                 # urlparse doesn't recognise spaces as URL delimiter
    
    196 225
                 log.debug('Text is not an uri: %s...', text[:15])
    
    ... ... @@ -211,7 +240,7 @@ def get_previewable(self, text, additional_data):
    211 240
     
    
    212 241
             return True
    
    213 242
     
    
    214
    -    def create_preview(self, uri, widget, context):
    
    243
    +    def create_preview(self, uri: str, widget: Any, context: str) -> None:
    
    215 244
             if uri.startswith('geo:'):
    
    216 245
                 preview = Preview(uri, None, None, None, 96, widget)
    
    217 246
                 preview.update_widget()
    
    ... ... @@ -238,7 +267,7 @@ def create_preview(self, uri, widget, context):
    238 267
                                 self._on_thumb_load_finished,
    
    239 268
                                 preview)
    
    240 269
     
    
    241
    -    def _process_web_uri(self, uri, widget):
    
    270
    +    def _process_web_uri(self, uri: str, widget: Any) -> Preview:
    
    242 271
             urlparts = urlparse(uri)
    
    243 272
             size = app.settings.get('preview_size')
    
    244 273
             orig_path, thumb_path = get_image_paths(uri,
    
    ... ... @@ -253,7 +282,13 @@ def _process_web_uri(self, uri, widget):
    253 282
                            size,
    
    254 283
                            widget)
    
    255 284
     
    
    256
    -    def _on_orig_load_finished(self, data, error, preview):
    
    285
    +    def _on_orig_load_finished(self,
    
    286
    +                               data: Optional[bytes],
    
    287
    +                               error: Gio.AsyncResult,
    
    288
    +                               preview: Preview) -> None:
    
    289
    +        if preview.thumb_path is None or preview.orig_path is None:
    
    290
    +            return
    
    291
    +
    
    257 292
             if data is None:
    
    258 293
                 log.error('%s: %s', preview.orig_path.name, error)
    
    259 294
                 return
    
    ... ... @@ -270,7 +305,12 @@ def _on_orig_load_finished(self, data, error, preview):
    270 305
                 preview.update_widget()
    
    271 306
     
    
    272 307
         @staticmethod
    
    273
    -    def _on_thumb_load_finished(data, error, preview):
    
    308
    +    def _on_thumb_load_finished(data: Optional[bytes],
    
    309
    +                                error: Gio.AsyncResult,
    
    310
    +                                preview: Preview) -> None:
    
    311
    +        if preview.thumb_path is None or preview.orig_path is None:
    
    312
    +            return
    
    313
    +
    
    274 314
             if data is None:
    
    275 315
                 log.error('%s: %s', preview.thumb_path.name, error)
    
    276 316
                 return
    
    ... ... @@ -288,7 +328,9 @@ def _on_thumb_load_finished(data, error, preview):
    288 328
                 return
    
    289 329
             preview.update_widget(data=pixbuf)
    
    290 330
     
    
    291
    -    def download_content(self, preview, force=False):
    
    331
    +    def download_content(self, 
    
    332
    +                         preview: Preview,
    
    333
    +                         force: bool = False) -> None:
    
    292 334
             if preview.account is None:
    
    293 335
                 # History Window can be opened without account context
    
    294 336
                 # This means we can not apply proxy settings
    
    ... ... @@ -302,7 +344,9 @@ def download_content(self, preview, force=False):
    302 344
             session = self._get_session(preview.account)
    
    303 345
             session.queue_message(message, self._on_finished, preview)
    
    304 346
     
    
    305
    -    def _check_certificate(self, message, preview):
    
    347
    +    def _check_certificate(self, 
    
    348
    +                           message: Soup.Message,
    
    349
    +                           preview: Preview) -> None:
    
    306 350
             _https_used, _tls_certificate, tls_errors = message.get_https_status()
    
    307 351
     
    
    308 352
             if not app.settings.get('preview_verify_https'):
    
    ... ... @@ -315,7 +359,12 @@ def _check_certificate(self, message, preview):
    315 359
                 session.cancel_message(message, Soup.Status.CANCELLED)
    
    316 360
                 return
    
    317 361
     
    
    318
    -    def _on_content_sniffed(self, message, type_, _params, preview, force):
    
    362
    +    def _on_content_sniffed(self, 
    
    363
    +                            message: Soup.Message,
    
    364
    +                            type_: str,
    
    365
    +                            _params: GLib.HashTable,
    
    366
    +                            preview: Preview,
    
    367
    +                            force: bool) -> None:
    
    319 368
             file_size = message.props.response_headers.get_content_length()
    
    320 369
             uri = message.props.uri.to_string(False)
    
    321 370
             session = self._get_session(preview.account)
    
    ... ... @@ -337,7 +386,10 @@ def _on_content_sniffed(self, message, type_, _params, preview, force):
    337 386
     
    
    338 387
             preview.update_widget()
    
    339 388
     
    
    340
    -    def _on_finished(self, _session, message, preview):
    
    389
    +    def _on_finished(self,
    
    390
    +                     _session: Soup.Session,
    
    391
    +                     message: Soup.Message,
    
    392
    +                     preview: Preview) -> None:
    
    341 393
             if message.status_code != Soup.Status.OK:
    
    342 394
                 log.warning('Download failed: %s', preview.request_uri)
    
    343 395
                 log.warning(Soup.Status.get_phrase(message.status_code))
    
    ... ... @@ -349,10 +401,12 @@ def _on_finished(self, _session, message, preview):
    349 401
                 return
    
    350 402
     
    
    351 403
             if preview.is_aes_encrypted:
    
    352
    -            data = aes_decrypt(preview, data)
    
    404
    +            if preview.key is not None and preview.iv is not None:
    
    405
    +                data = aes_decrypt(preview.key, preview.iv, data)
    
    353 406
     
    
    354 407
             if preview.mime_type == 'application/octet-stream':
    
    355
    -            preview.mime_type = guess_mime_type(preview.orig_path, data)
    
    408
    +            if preview.orig_path is not None:
    
    409
    +                preview.mime_type = guess_mime_type(preview.orig_path, data)
    
    356 410
     
    
    357 411
             write_file_async(preview.orig_path,
    
    358 412
                              data,
    
    ... ... @@ -367,7 +421,12 @@ def _on_finished(self, _session, message, preview):
    367 421
                                      preview)
    
    368 422
     
    
    369 423
         @staticmethod
    
    370
    -    def _on_orig_write_finished(_result, error, preview):
    
    424
    +    def _on_orig_write_finished(_result: bool,
    
    425
    +                                error: GLib.Error,
    
    426
    +                                preview: Preview) -> None:
    
    427
    +        if preview.orig_path is None:
    
    428
    +            return
    
    429
    +
    
    371 430
             if error is not None:
    
    372 431
                 log.error('%s: %s', preview.orig_path.name, error)
    
    373 432
                 return
    
    ... ... @@ -380,13 +439,21 @@ def _on_orig_write_finished(_result, error, preview):
    380 439
                 preview.update_widget()
    
    381 440
     
    
    382 441
         @staticmethod
    
    383
    -    def _on_thumb_write_finished(_result, error, preview):
    
    442
    +    def _on_thumb_write_finished(_result: bool,
    
    443
    +                                 error: GLib.Error,
    
    444
    +                                 preview: Preview) -> None:
    
    445
    +        if preview.thumb_path is None:
    
    446
    +            return
    
    447
    +
    
    384 448
             if error is not None:
    
    385 449
                 log.error('%s: %s', preview.thumb_path.name, error)
    
    386 450
                 return
    
    387 451
     
    
    388 452
             log.info('Thumbnail stored: %s ', preview.thumb_path.name)
    
    389 453
     
    
    454
    +        if preview.thumbnail is None:
    
    455
    +            return
    
    456
    +
    
    390 457
             try:
    
    391 458
                 pixbuf = pixbuf_from_data(preview.thumbnail)
    
    392 459
             except Exception as err:
    

  • gajim/common/preview_helpers.py
    ... ... @@ -12,9 +12,13 @@
    12 12
     # You should have received a copy of the GNU General Public License
    
    13 13
     # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14 14
     
    
    15
    +from typing import Any
    
    16
    +from typing import Dict
    
    15 17
     from typing import List
    
    16 18
     from typing import Tuple
    
    17 19
     from typing import Optional
    
    20
    +from typing import Union
    
    21
    +
    
    18 22
     import math
    
    19 23
     import logging
    
    20 24
     import binascii
    
    ... ... @@ -25,6 +29,7 @@
    25 29
     from pathlib import Path
    
    26 30
     from urllib.parse import urlparse
    
    27 31
     from urllib.parse import unquote
    
    32
    +from urllib.parse import ParseResult
    
    28 33
     
    
    29 34
     from gi.repository import GdkPixbuf
    
    30 35
     from gi.repository import GLib
    
    ... ... @@ -45,7 +50,8 @@
    45 50
     Coords = namedtuple('Coords', 'location lat lon')
    
    46 51
     
    
    47 52
     
    
    48
    -def resize_gif(image, output_file, resize_to):
    
    53
    +def resize_gif(image: Image.Image, output_file: BytesIO,
    
    54
    +               resize_to: Tuple[int, int]) -> None:
    
    49 55
         frames, result = extract_and_resize_frames(image, resize_to)
    
    50 56
     
    
    51 57
         frames[0].save(output_file,
    
    ... ... @@ -57,7 +63,7 @@ def resize_gif(image, output_file, resize_to):
    57 63
                        loop=1000)
    
    58 64
     
    
    59 65
     
    
    60
    -def analyse_image(image):
    
    66
    +def analyse_image(image: Image.Image) -> Tuple[Image.Image, Dict[str, Any]]:
    
    61 67
         '''
    
    62 68
         Pre-process pass over the image to determine the mode (full or additive).
    
    63 69
         Necessary as assessing single frames isn't reliable. Need to know the mode
    
    ... ... @@ -85,7 +91,8 @@ def analyse_image(image):
    85 91
         return image, result
    
    86 92
     
    
    87 93
     
    
    88
    -def extract_and_resize_frames(image, resize_to):
    
    94
    +def extract_and_resize_frames(image: Image.Image, resize_to: Tuple[int, int]
    
    95
    +                              ) -> Tuple[List[Image.Image], Dict[str, Any]]:
    
    89 96
         image, result = analyse_image(image)
    
    90 97
     
    
    91 98
         i = 0
    
    ... ... @@ -242,7 +249,7 @@ def pixbuf_from_data(data: bytes) -> GdkPixbuf.Pixbuf:
    242 249
         return loader.get_pixbuf().apply_embedded_orientation()
    
    243 250
     
    
    244 251
     
    
    245
    -def parse_fragment(fragment):
    
    252
    +def parse_fragment(fragment: str) -> Tuple[bytes, bytes]:
    
    246 253
         if not fragment:
    
    247 254
             raise ValueError('Invalid fragment')
    
    248 255
     
    
    ... ... @@ -263,7 +270,8 @@ def parse_fragment(fragment):
    263 270
         return key, iv
    
    264 271
     
    
    265 272
     
    
    266
    -def get_image_paths(uri, urlparts, size, orig_dir, thumb_dir):
    
    273
    +def get_image_paths(uri: str, urlparts: ParseResult, size: int, orig_dir: Path,
    
    274
    +                    thumb_dir: Path) -> Tuple[Path, Path]:
    
    267 275
         path = Path(unquote(urlparts.path))
    
    268 276
         web_stem = path.stem
    
    269 277
         extension = path.suffix
    
    ... ... @@ -327,18 +335,17 @@ def filename_from_uri(uri: str) -> str:
    327 335
         return path.name
    
    328 336
     
    
    329 337
     
    
    330
    -def aes_decrypt(preview, payload):
    
    338
    +def aes_decrypt(key: bytes, iv: bytes, payload: bytes) -> bytes:
    
    331 339
         # Use AES128 GCM with the given key and iv to decrypt the payload
    
    332 340
         data = payload[:-16]
    
    333 341
         tag = payload[-16:]
    
    334
    -    decryptor = Cipher(
    
    335
    -        algorithms.AES(preview.key),
    
    336
    -        GCM(preview.iv, tag=tag),
    
    337
    -        backend=default_backend()).decryptor()
    
    342
    +    decryptor = Cipher(algorithms.AES(key),
    
    343
    +                       GCM(iv, tag=tag),
    
    344
    +                       backend=default_backend()).decryptor()
    
    338 345
         return decryptor.update(data) + decryptor.finalize()
    
    339 346
     
    
    340 347
     
    
    341
    -def contains_audio_streams(file_path):
    
    348
    +def contains_audio_streams(file_path: Path) -> bool:
    
    342 349
         # Check if it is really an audio file
    
    343 350
         has_audio = False
    
    344 351
         discoverer = GstPbutils.Discoverer()
    
    ... ... @@ -368,14 +375,15 @@ def get_previewable_mime_types():
    368 375
         ))
    
    369 376
     
    
    370 377
     
    
    371
    -def guess_mime_type(file_path: str,
    
    378
    +def guess_mime_type(file_path: Union[Path, str],
    
    372 379
                         data: Optional[bytes] = None
    
    373 380
                         ) -> str:
    
    374
    -    mime_type, _ = mimetypes.MimeTypes().guess_type(str(file_path))
    
    381
    +    file_path = str(file_path)
    
    382
    +    mime_type, _ = mimetypes.MimeTypes().guess_type(file_path)
    
    375 383
         if mime_type is None:
    
    376 384
             # Try to guess MIME type by file name
    
    377
    -        mime_type, _ = Gio.content_type_guess(str(file_path), data)
    
    378
    -    log.debug('Guessed MIME type: %s', str(mime_type))
    
    385
    +        mime_type, _ = Gio.content_type_guess(file_path, data)
    
    386
    +    log.debug('Guessed MIME type: %s', mime_type)
    
    379 387
         return mime_type
    
    380 388
     
    
    381 389