Merge pull request 'Matrix Integration' (#217) from matrix-integration into master

Reviewed-on: platypush/platypush#217

Closes: #2
This commit is contained in:
Fabio Manganiello 2022-08-28 15:21:05 +02:00
commit f4360dc0e0
15 changed files with 2623 additions and 171 deletions

View file

@ -138,15 +138,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
@ -156,8 +153,7 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'platypush.tex', 'platypush Documentation', (master_doc, 'platypush.tex', 'platypush Documentation', 'BlackLight', 'manual'),
'BlackLight', 'manual'),
] ]
@ -165,10 +161,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, 'platypush', 'platypush Documentation', [author], 1)]
(master_doc, 'platypush', 'platypush Documentation',
[author], 1)
]
# -- Options for Texinfo output ---------------------------------------------- # -- Options for Texinfo output ----------------------------------------------
@ -177,9 +170,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'platypush', 'platypush Documentation', (
author, 'platypush', 'One line description of project.', master_doc,
'Miscellaneous'), 'platypush',
'platypush Documentation',
author,
'platypush',
'One line description of project.',
'Miscellaneous',
),
] ]
@ -199,7 +198,8 @@ autodoc_default_options = {
'inherited-members': True, 'inherited-members': True,
} }
autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', autodoc_mock_imports = [
'googlesamples.assistant.grpc.audio_helpers',
'google.assistant.embedded', 'google.assistant.embedded',
'google.assistant.library', 'google.assistant.library',
'google.assistant.library.event', 'google.assistant.library.event',
@ -291,6 +291,9 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
'irc.connection', 'irc.connection',
'irc.events', 'irc.events',
'defusedxml', 'defusedxml',
'nio',
'aiofiles',
'aiofiles.os',
] ]
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../..'))

View file

@ -41,6 +41,7 @@ Events
platypush/events/linode.rst platypush/events/linode.rst
platypush/events/log.http.rst platypush/events/log.http.rst
platypush/events/mail.rst platypush/events/mail.rst
platypush/events/matrix.rst
platypush/events/media.rst platypush/events/media.rst
platypush/events/midi.rst platypush/events/midi.rst
platypush/events/mqtt.rst platypush/events/mqtt.rst
@ -72,6 +73,7 @@ Events
platypush/events/weather.rst platypush/events/weather.rst
platypush/events/web.rst platypush/events/web.rst
platypush/events/web.widget.rst platypush/events/web.widget.rst
platypush/events/websocket.rst
platypush/events/wiimote.rst platypush/events/wiimote.rst
platypush/events/zeroborg.rst platypush/events/zeroborg.rst
platypush/events/zeroconf.rst platypush/events/zeroconf.rst

View file

@ -0,0 +1,5 @@
``matrix``
==========
.. automodule:: platypush.message.event.matrix
:members:

View file

@ -0,0 +1,5 @@
``websocket``
=============
.. automodule:: platypush.message.event.websocket
:members:

View file

@ -0,0 +1,5 @@
``matrix``
==========
.. automodule:: platypush.plugins.matrix
:members:

View file

@ -75,6 +75,7 @@ Plugins
platypush/plugins/mail.smtp.rst platypush/plugins/mail.smtp.rst
platypush/plugins/mailgun.rst platypush/plugins/mailgun.rst
platypush/plugins/mastodon.rst platypush/plugins/mastodon.rst
platypush/plugins/matrix.rst
platypush/plugins/media.chromecast.rst platypush/plugins/media.chromecast.rst
platypush/plugins/media.gstreamer.rst platypush/plugins/media.gstreamer.rst
platypush/plugins/media.jellyfin.rst platypush/plugins/media.jellyfin.rst

View file

@ -213,11 +213,10 @@ class Config:
config['scripts_dir'] = os.path.abspath( config['scripts_dir'] = os.path.abspath(
os.path.expanduser(file_config[section]) os.path.expanduser(file_config[section])
) )
elif ( else:
'disabled' not in file_config[section] section_config = file_config.get(section, {}) or {}
or file_config[section]['disabled'] is False if not section_config.get('disabled'):
): config[section] = section_config
config[section] = file_config[section]
return config return config

View file

@ -0,0 +1,250 @@
from datetime import datetime
from typing import Dict, Any
from platypush.message.event import Event
class MatrixEvent(Event):
"""
Base matrix event.
"""
def __init__(
self,
*args,
server_url: str,
sender_id: str | None = None,
sender_display_name: str | None = None,
sender_avatar_url: str | None = None,
room_id: str | None = None,
room_name: str | None = None,
room_topic: str | None = None,
server_timestamp: datetime | None = None,
**kwargs
):
"""
:param server_url: Base server URL.
:param sender_id: The event's sender ID.
:param sender_display_name: The event's sender display name.
:param sender_avatar_url: The event's sender avatar URL.
:param room_id: Event room ID.
:param room_name: The name of the room associated to the event.
:param room_topic: The topic of the room associated to the event.
:param server_timestamp: The server timestamp of the event.
"""
evt_args: Dict[str, Any] = {
'server_url': server_url,
}
if sender_id:
evt_args['sender_id'] = sender_id
if sender_display_name:
evt_args['sender_display_name'] = sender_display_name
if sender_avatar_url:
evt_args['sender_avatar_url'] = sender_avatar_url
if room_id:
evt_args['room_id'] = room_id
if room_name:
evt_args['room_name'] = room_name
if room_topic:
evt_args['room_topic'] = room_topic
if server_timestamp:
evt_args['server_timestamp'] = server_timestamp
super().__init__(*args, **evt_args, **kwargs)
class MatrixSyncEvent(MatrixEvent):
"""
Event triggered when the startup synchronization has been completed and the
plugin is ready to use.
"""
class MatrixMessageEvent(MatrixEvent):
"""
Event triggered when a message is received on a subscribed room.
"""
def __init__(
self,
*args,
body: str = '',
url: str | None = None,
thumbnail_url: str | None = None,
mimetype: str | None = None,
formatted_body: str | None = None,
format: str | None = None,
**kwargs
):
"""
:param body: The body of the message.
:param url: The URL of the media file, if the message includes media.
:param thumbnail_url: The URL of the thumbnail, if the message includes media.
:param mimetype: The MIME type of the media file, if the message includes media.
:param formatted_body: The formatted body, if ``format`` is specified.
:param format: The format of the message (e.g. ``html`` or ``markdown``).
"""
super().__init__(
*args,
body=body,
url=url,
thumbnail_url=thumbnail_url,
mimetype=mimetype,
formatted_body=formatted_body,
format=format,
**kwargs
)
class MatrixMessageImageEvent(MatrixEvent):
"""
Event triggered when a message containing an image is received.
"""
class MatrixMessageFileEvent(MatrixEvent):
"""
Event triggered when a message containing a generic file is received.
"""
class MatrixMessageAudioEvent(MatrixEvent):
"""
Event triggered when a message containing an audio file is received.
"""
class MatrixMessageVideoEvent(MatrixEvent):
"""
Event triggered when a message containing a video file is received.
"""
class MatrixReactionEvent(MatrixEvent):
"""
Event triggered when a user submits a reaction to an event.
"""
def __init__(self, *args, in_response_to_event_id: str, **kwargs):
"""
:param in_response_to_event_id: The ID of the URL related to the reaction.
"""
super().__init__(
*args, in_response_to_event_id=in_response_to_event_id, **kwargs
)
class MatrixEncryptedMessageEvent(MatrixMessageEvent):
"""
Event triggered when a message is received but the client doesn't
have the E2E keys to decrypt it, or encryption has not been enabled.
"""
class MatrixCallEvent(MatrixEvent):
"""
Base class for Matrix call events.
"""
def __init__(
self, *args, call_id: str, version: int, sdp: str | None = None, **kwargs
):
"""
:param call_id: The unique ID of the call.
:param version: An increasing integer representing the version of the call.
:param sdp: SDP text of the session description.
"""
super().__init__(*args, call_id=call_id, version=version, sdp=sdp, **kwargs)
class MatrixCallInviteEvent(MatrixCallEvent):
"""
Event triggered when the user is invited to a call.
"""
def __init__(self, *args, invite_validity: float | None = None, **kwargs):
"""
:param invite_validity: For how long the invite will be valid, in seconds.
:param sdp: SDP text of the session description.
"""
super().__init__(*args, invite_validity=invite_validity, **kwargs)
class MatrixCallAnswerEvent(MatrixCallEvent):
"""
Event triggered by the callee when they wish to answer the call.
"""
class MatrixCallHangupEvent(MatrixCallEvent):
"""
Event triggered when a participant in the call exists.
"""
class MatrixRoomCreatedEvent(MatrixEvent):
"""
Event triggered when a room is created.
"""
class MatrixRoomJoinEvent(MatrixEvent):
"""
Event triggered when a user joins a room.
"""
class MatrixRoomLeaveEvent(MatrixEvent):
"""
Event triggered when a user leaves a room.
"""
class MatrixRoomInviteEvent(MatrixEvent):
"""
Event triggered when the user is invited to a room.
"""
class MatrixRoomTopicChangedEvent(MatrixEvent):
"""
Event triggered when the topic/title of a room changes.
"""
def __init__(self, *args, topic: str, **kwargs):
"""
:param topic: New room topic.
"""
super().__init__(*args, topic=topic, **kwargs)
class MatrixRoomTypingStartEvent(MatrixEvent):
"""
Event triggered when a user in a room starts typing.
"""
class MatrixRoomTypingStopEvent(MatrixEvent):
"""
Event triggered when a user in a room stops typing.
"""
class MatrixRoomSeenReceiptEvent(MatrixEvent):
"""
Event triggered when the last message seen by a user in a room is updated.
"""
class MatrixUserPresenceEvent(MatrixEvent):
"""
Event triggered when a user comes online or goes offline.
"""
def __init__(self, *args, is_active: bool, last_active: datetime | None, **kwargs):
"""
:param is_active: True if the user is currently online.
:param topic: When the user was last active.
"""
super().__init__(*args, is_active=is_active, last_active=last_active, **kwargs)

View file

@ -12,8 +12,12 @@ from platypush.config import Config
from platypush.context import get_plugin from platypush.context import get_plugin
from platypush.message import Message from platypush.message import Message
from platypush.message.response import Response from platypush.message.response import Response
from platypush.utils import get_hash, get_module_and_method_from_action, get_redis_queue_name_by_message, \ from platypush.utils import (
is_functional_procedure get_hash,
get_module_and_method_from_action,
get_redis_queue_name_by_message,
is_functional_procedure,
)
logger = logging.getLogger('platypush') logger = logging.getLogger('platypush')
@ -21,8 +25,17 @@ logger = logging.getLogger('platypush')
class Request(Message): class Request(Message):
"""Request message class""" """Request message class"""
def __init__(self, target, action, origin=None, id=None, backend=None, def __init__(
args=None, token=None, timestamp=None): self,
target,
action,
origin=None,
id=None,
backend=None,
args=None,
token=None,
timestamp=None,
):
""" """
Params: Params:
target -- Target node [Str] target -- Target node [Str]
@ -48,9 +61,13 @@ class Request(Message):
@classmethod @classmethod
def build(cls, msg): def build(cls, msg):
msg = super().parse(msg) msg = super().parse(msg)
args = {'target': msg.get('target', Config.get('device_id')), 'action': msg['action'], args = {
'args': msg.get('args', {}), 'id': msg['id'] if 'id' in msg else cls._generate_id(), 'target': msg.get('target', Config.get('device_id')),
'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time()} 'action': msg['action'],
'args': msg.get('args', {}),
'id': msg['id'] if 'id' in msg else cls._generate_id(),
'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time(),
}
if 'origin' in msg: if 'origin' in msg:
args['origin'] = msg['origin'] args['origin'] = msg['origin']
@ -61,7 +78,7 @@ class Request(Message):
@staticmethod @staticmethod
def _generate_id(): def _generate_id():
_id = '' _id = ''
for i in range(0, 16): for _ in range(0, 16):
_id += '%.2x' % random.randint(0, 255) _id += '%.2x' % random.randint(0, 255)
return _id return _id
@ -84,9 +101,14 @@ class Request(Message):
return proc_config(*args, **kwargs) return proc_config(*args, **kwargs)
proc = Procedure.build(name=proc_name, requests=proc_config['actions'], proc = Procedure.build(
_async=proc_config['_async'], args=self.args, name=proc_name,
backend=self.backend, id=self.id) requests=proc_config['actions'],
_async=proc_config['_async'],
args=self.args,
backend=self.backend,
id=self.id,
)
return proc.execute(*args, **kwargs) return proc.execute(*args, **kwargs)
@ -112,7 +134,7 @@ class Request(Message):
if isinstance(value, str): if isinstance(value, str):
value = self.expand_value_from_context(value, **context) value = self.expand_value_from_context(value, **context)
elif isinstance(value, dict) or isinstance(value, list): elif isinstance(value, (dict, list)):
self._expand_context(event_args=value, **context) self._expand_context(event_args=value, **context)
event_args[key] = value event_args[key] = value
@ -132,7 +154,11 @@ class Request(Message):
try: try:
exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v))) exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v)))
except Exception as e: except Exception as e:
logger.debug('Could not set context variable {}={}: {}'.format(k, v, str(e))) logger.debug(
'Could not set context variable {}={}: {}'.format(
k, v, str(e)
)
)
logger.debug('Context: {}'.format(context)) logger.debug('Context: {}'.format(context))
parsed_value = '' parsed_value = ''
@ -152,7 +178,7 @@ class Request(Message):
if callable(context_value): if callable(context_value):
context_value = context_value() context_value = context_value()
if isinstance(context_value, range) or isinstance(context_value, tuple): if isinstance(context_value, (range, tuple)):
context_value = [*context_value] context_value = [*context_value]
if isinstance(context_value, datetime.date): if isinstance(context_value, datetime.date):
context_value = context_value.isoformat() context_value = context_value.isoformat()
@ -162,7 +188,7 @@ class Request(Message):
parsed_value += prefix + ( parsed_value += prefix + (
json.dumps(context_value) json.dumps(context_value)
if isinstance(context_value, list) or isinstance(context_value, dict) if isinstance(context_value, (list, dict))
else str(context_value) else str(context_value)
) )
else: else:
@ -205,6 +231,9 @@ class Request(Message):
""" """
def _thread_func(_n_tries, errors=None): def _thread_func(_n_tries, errors=None):
from platypush.context import get_bus
from platypush.plugins import RunnablePlugin
response = None response = None
try: try:
@ -221,11 +250,15 @@ class Request(Message):
return response return response
else: else:
action = self.expand_value_from_context(self.action, **context) action = self.expand_value_from_context(self.action, **context)
(module_name, method_name) = get_module_and_method_from_action(action) (module_name, method_name) = get_module_and_method_from_action(
action
)
plugin = get_plugin(module_name) plugin = get_plugin(module_name)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
msg = 'Uncaught pre-processing exception from action [{}]: {}'.format(self.action, str(e)) msg = 'Uncaught pre-processing exception from action [{}]: {}'.format(
self.action, str(e)
)
logger.warning(msg) logger.warning(msg)
response = Response(output=None, errors=[msg]) response = Response(output=None, errors=[msg])
self._send_response(response) self._send_response(response)
@ -243,24 +276,36 @@ class Request(Message):
response = plugin.run(method_name, args) response = plugin.run(method_name, args)
if not response: if not response:
logger.warning('Received null response from action {}'.format(action)) logger.warning(
'Received null response from action {}'.format(action)
)
else: else:
if response.is_error(): if response.is_error():
logger.warning(('Response processed with errors from ' + logger.warning(
'action {}: {}').format( (
action, str(response))) 'Response processed with errors from ' + 'action {}: {}'
).format(action, str(response))
)
elif not response.disable_logging: elif not response.disable_logging:
logger.info('Processed response from action {}: {}'. logger.info(
format(action, str(response))) 'Processed response from action {}: {}'.format(
action, str(response)
)
)
except (AssertionError, TimeoutError) as e: except (AssertionError, TimeoutError) as e:
plugin.logger.exception(e) logger.warning(
logger.warning('{} from action [{}]: {}'.format(type(e), action, str(e))) '%s from action [%s]: %s', e.__class__.__name__, action, str(e)
)
response = Response(output=None, errors=[str(e)]) response = Response(output=None, errors=[str(e)])
except Exception as e: except Exception as e:
# Retry mechanism # Retry mechanism
plugin.logger.exception(e) plugin.logger.exception(e)
logger.warning(('Uncaught exception while processing response ' + logger.warning(
'from action [{}]: {}').format(action, str(e))) (
'Uncaught exception while processing response '
+ 'from action [{}]: {}'
).format(action, str(e))
)
errors = errors or [] errors = errors or []
if str(e) not in errors: if str(e) not in errors:
@ -269,16 +314,20 @@ class Request(Message):
response = Response(output=None, errors=errors) response = Response(output=None, errors=errors)
if _n_tries - 1 > 0: if _n_tries - 1 > 0:
logger.info('Reloading plugin {} and retrying'.format(module_name)) logger.info('Reloading plugin {} and retrying'.format(module_name))
get_plugin(module_name, reload=True) plugin = get_plugin(module_name, reload=True)
if isinstance(plugin, RunnablePlugin):
plugin.bus = get_bus()
plugin.start()
response = _thread_func(_n_tries=_n_tries - 1, errors=errors) response = _thread_func(_n_tries=_n_tries - 1, errors=errors)
finally: finally:
self._send_response(response) self._send_response(response)
return response return response
token_hash = Config.get('token_hash') stored_token_hash = Config.get('token_hash')
token = getattr(self, 'token', '')
if token_hash: if stored_token_hash and get_hash(token) != stored_token_hash:
if self.token is None or get_hash(self.token) != token_hash:
raise PermissionError() raise PermissionError()
if _async: if _async:
@ -292,7 +341,8 @@ class Request(Message):
the message into a UTF-8 JSON string the message into a UTF-8 JSON string
""" """
return json.dumps({ return json.dumps(
{
'type': 'request', 'type': 'request',
'target': self.target, 'target': self.target,
'action': self.action, 'action': self.action,
@ -301,6 +351,8 @@ class Request(Message):
'id': self.id if hasattr(self, 'id') else None, 'id': self.id if hasattr(self, 'id') else None,
'token': self.token if hasattr(self, 'token') else None, 'token': self.token if hasattr(self, 'token') else None,
'_timestamp': self.timestamp, '_timestamp': self.timestamp,
}) }
)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -21,12 +21,12 @@ def action(f):
result = f(*args, **kwargs) result = f(*args, **kwargs)
if result and isinstance(result, Response): if result and isinstance(result, Response):
result.errors = result.errors \ result.errors = (
if isinstance(result.errors, list) else [result.errors] result.errors if isinstance(result.errors, list) else [result.errors]
)
response = result response = result
elif isinstance(result, tuple) and len(result) == 2: elif isinstance(result, tuple) and len(result) == 2:
response.errors = result[1] \ response.errors = result[1] if isinstance(result[1], list) else [result[1]]
if isinstance(result[1], list) else [result[1]]
if len(response.errors) == 1 and response.errors[0] is None: if len(response.errors) == 1 and response.errors[0] is None:
response.errors = [] response.errors = []
@ -46,7 +46,9 @@ class Plugin(EventGenerator, ExtensionWithManifest): # lgtm [py/missing-call-t
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__() super().__init__()
self.logger = logging.getLogger('platypush:plugin:' + get_plugin_name_by_class(self.__class__)) self.logger = logging.getLogger(
'platypush:plugin:' + get_plugin_name_by_class(self.__class__)
)
if 'logging' in kwargs: if 'logging' in kwargs:
self.logger.setLevel(getattr(logging, kwargs['logging'].upper())) self.logger.setLevel(getattr(logging, kwargs['logging'].upper()))
@ -55,8 +57,9 @@ class Plugin(EventGenerator, ExtensionWithManifest): # lgtm [py/missing-call-t
) )
def run(self, method, *args, **kwargs): def run(self, method, *args, **kwargs):
assert method in self.registered_actions, '{} is not a registered action on {}'.\ assert (
format(method, self.__class__.__name__) method in self.registered_actions
), '{} is not a registered action on {}'.format(method, self.__class__.__name__)
return getattr(self, method)(*args, **kwargs) return getattr(self, method)(*args, **kwargs)
@ -64,6 +67,7 @@ class RunnablePlugin(Plugin):
""" """
Class for runnable plugins - i.e. plugins that have a start/stop method and can be started. Class for runnable plugins - i.e. plugins that have a start/stop method and can be started.
""" """
def __init__(self, poll_interval: Optional[float] = None, **kwargs): def __init__(self, poll_interval: Optional[float] = None, **kwargs):
""" """
:param poll_interval: How often the :meth:`.loop` function should be execute (default: None, no pause/interval). :param poll_interval: How often the :meth:`.loop` function should be execute (default: None, no pause/interval).
@ -80,6 +84,9 @@ class RunnablePlugin(Plugin):
def should_stop(self): def should_stop(self):
return self._should_stop.is_set() return self._should_stop.is_set()
def wait_stop(self, timeout=None):
return self._should_stop.wait(timeout=timeout)
def start(self): def start(self):
set_thread_name(self.__class__.__name__) set_thread_name(self.__class__.__name__)
self._thread = threading.Thread(target=self._runner) self._thread = threading.Thread(target=self._runner)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
manifest:
events:
platypush.message.event.matrix.MatrixMessageEvent: when a message is
received.
platypush.message.event.matrix.MatrixMessageImageEvent: when a message
containing an image is received.
platypush.message.event.matrix.MatrixMessageAudioEvent: when a message
containing an audio file is received.
platypush.message.event.matrix.MatrixMessageVideoEvent: when a message
containing a video file is received.
platypush.message.event.matrix.MatrixMessageFileEvent: when a message
containing a generic file is received.
platypush.message.event.matrix.MatrixSyncEvent: when the startup
synchronization has been completed and the plugin is ready to use.
platypush.message.event.matrix.MatrixRoomCreatedEvent: when a room is
created.
platypush.message.event.matrix.MatrixRoomJoinEvent: when a user joins a
room.
platypush.message.event.matrix.MatrixRoomLeaveEvent: when a user leaves a
room.
platypush.message.event.matrix.MatrixRoomInviteEvent: when the user is
invited to a room.
platypush.message.event.matrix.MatrixRoomTopicChangedEvent: when the
topic/title of a room changes.
platypush.message.event.matrix.MatrixCallInviteEvent: when the user is
invited to a call.
platypush.message.event.matrix.MatrixCallAnswerEvent: when a called user
wishes to pick the call.
platypush.message.event.matrix.MatrixCallHangupEvent: when a called user
exits the call.
platypush.message.event.matrix.MatrixEncryptedMessageEvent: when a message
is received but the client doesn't have the E2E keys to decrypt it, or
encryption has not been enabled.
platypush.message.event.matrix.MatrixRoomTypingStartEvent: when a user in a
room starts typing.
platypush.message.event.matrix.MatrixRoomTypingStopEvent: when a user in a
room stops typing.
platypush.message.event.matrix.MatrixRoomSeenReceiptEvent: when the last
message seen by a user in a room is updated.
platypush.message.event.matrix.MatrixUserPresenceEvent: when a user comes
online or goes offline.
apt:
- libolm-devel
pacman:
- libolm
pip:
- matrix-nio[e2e]
- async_lru
package: platypush.plugins.matrix
type: plugin

View file

@ -21,7 +21,17 @@ class StrippedString(fields.Function): # lgtm [py/missing-call-to-init]
return value.strip() return value.strip()
class DateTime(fields.Function): # lgtm [py/missing-call-to-init] class Function(fields.Function): # lgtm [py/missing-call-to-init]
def _get_attr(self, obj, attr: str, _recursive=True):
if self.attribute and _recursive:
return self._get_attr(obj, self.attribute, False)
if hasattr(obj, attr):
return getattr(obj, attr)
elif hasattr(obj, 'get'):
return obj.get(attr)
class DateTime(Function): # lgtm [py/missing-call-to-init]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.metadata = { self.metadata = {
@ -30,7 +40,7 @@ class DateTime(fields.Function): # lgtm [py/missing-call-to-init]
} }
def _serialize(self, value, attr, obj, **kwargs) -> Optional[str]: def _serialize(self, value, attr, obj, **kwargs) -> Optional[str]:
value = normalize_datetime(obj.get(attr)) value = normalize_datetime(self._get_attr(obj, attr))
if value: if value:
return value.isoformat() return value.isoformat()
@ -38,7 +48,7 @@ class DateTime(fields.Function): # lgtm [py/missing-call-to-init]
return normalize_datetime(value) return normalize_datetime(value)
class Date(fields.Function): # lgtm [py/missing-call-to-init] class Date(Function): # lgtm [py/missing-call-to-init]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.metadata = { self.metadata = {
@ -47,7 +57,7 @@ class Date(fields.Function): # lgtm [py/missing-call-to-init]
} }
def _serialize(self, value, attr, obj, **kwargs) -> Optional[str]: def _serialize(self, value, attr, obj, **kwargs) -> Optional[str]:
value = normalize_datetime(obj.get(attr)) value = normalize_datetime(self._get_attr(obj, attr))
if value: if value:
return date(value.year, value.month, value.day).isoformat() return date(value.year, value.month, value.day).isoformat()
@ -56,10 +66,12 @@ class Date(fields.Function): # lgtm [py/missing-call-to-init]
return date.fromtimestamp(dt.timestamp()) return date.fromtimestamp(dt.timestamp())
def normalize_datetime(dt: Union[str, date, datetime]) -> Optional[Union[date, datetime]]: def normalize_datetime(
dt: Optional[Union[str, date, datetime]]
) -> Optional[Union[date, datetime]]:
if not dt: if not dt:
return return
if isinstance(dt, datetime) or isinstance(dt, date): if isinstance(dt, (datetime, date)):
return dt return dt
try: try:

385
platypush/schemas/matrix.py Normal file
View file

@ -0,0 +1,385 @@
from marshmallow import fields
from marshmallow.schema import Schema
from platypush.schemas import DateTime
class MillisecondsTimestamp(DateTime):
def _get_attr(self, *args, **kwargs):
value = super()._get_attr(*args, **kwargs)
if isinstance(value, int):
value = float(value / 1000)
return value
class MatrixEventIdSchema(Schema):
event_id = fields.String(
required=True,
metadata={
'description': 'Event ID',
'example': '$24KT_aQz6sSKaZH8oTCibRTl62qywDgQXMpz5epXsW5',
},
)
class MatrixRoomIdSchema(Schema):
room_id = fields.String(
required=True,
metadata={
'description': 'Room ID',
'example': '!aBcDeFgHiJkMnO:matrix.example.org',
},
)
class MatrixProfileSchema(Schema):
user_id = fields.String(
required=True,
metadata={
'description': 'User ID',
'example': '@myuser:matrix.example.org',
},
)
display_name = fields.String(
attribute='displayname',
metadata={
'description': 'User display name',
'example': 'Foo Bar',
},
)
avatar_url = fields.URL(
metadata={
'description': 'User avatar URL',
'example': 'mxc://matrix.example.org/AbCdEfG0123456789',
}
)
class MatrixMemberSchema(MatrixProfileSchema):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['display_name'].attribute = 'display_name'
class MatrixRoomSchema(Schema):
room_id = fields.String(
required=True,
metadata={
'description': 'Room ID',
'example': '!aBcDeFgHiJkMnO:matrix.example.org',
},
)
name = fields.String(
metadata={
'description': 'Room name',
'example': 'My Room',
}
)
display_name = fields.String(
metadata={
'description': 'Room display name',
'example': 'My Room',
}
)
topic = fields.String(
metadata={
'description': 'Room topic',
'example': 'My Room Topic',
}
)
avatar_url = fields.URL(
attribute='room_avatar_url',
metadata={
'description': 'Room avatar URL',
'example': 'mxc://matrix.example.org/AbCdEfG0123456789',
},
)
owner_id = fields.String(
attribute='own_user_id',
metadata={
'description': 'Owner user ID',
'example': '@myuser:matrix.example.org',
},
)
encrypted = fields.Bool()
class MatrixDeviceSchema(Schema):
device_id = fields.String(
required=True,
attribute='id',
metadata={
'description': 'ABCDEFG',
},
)
user_id = fields.String(
required=True,
metadata={
'description': 'User ID associated to the device',
'example': '@myuser:matrix.example.org',
},
)
display_name = fields.String(
metadata={
'description': 'Display name of the device',
'example': 'Element Android',
},
)
blacklisted = fields.Boolean()
deleted = fields.Boolean(default=False)
ignored = fields.Boolean()
verified = fields.Boolean()
keys = fields.Dict(
metadata={
'description': 'Encryption keys supported by the device',
'example': {
'curve25519': 'BtlB0vaQmtYFsvOYkmxyzw9qP5yGjuAyRh4gXh3q',
'ed25519': 'atohIK2FeVlYoY8xxpZ1bhDbveD+HA2DswNFqUxP',
},
},
)
class MatrixMyDeviceSchema(Schema):
device_id = fields.String(
required=True,
attribute='id',
metadata={
'description': 'ABCDEFG',
},
)
display_name = fields.String(
metadata={
'description': 'Device display name',
'example': 'My Device',
}
)
last_seen_ip = fields.String(
metadata={
'description': 'Last IP associated to this device',
'example': '1.2.3.4',
}
)
last_seen_date = DateTime(
metadata={
'description': 'The last time that the device was reported online',
'example': '2022-07-23T17:20:01.254223',
}
)
class MatrixDownloadedFileSchema(Schema):
url = fields.String(
metadata={
'description': 'Matrix URL of the original resource',
'example': 'mxc://matrix.example.org/YhQycHvFOvtiDDbEeWWtEhXx',
},
)
path = fields.String(
metadata={
'description': 'Local path where the file has been saved',
'example': '/home/user/Downloads/image.png',
}
)
content_type = fields.String(
metadata={
'description': 'Content type of the downloaded file',
'example': 'image/png',
}
)
size = fields.Int(
metadata={
'description': 'Length in bytes of the output file',
'example': 1024,
}
)
class MatrixMessageSchema(Schema):
event_id = fields.String(
required=True,
metadata={
'description': 'Event ID associated to this message',
'example': '$2eOQ5ueafANj91GnPCRkRUOOjM7dI5kFDOlfMNCD2ly',
},
)
room_id = fields.String(
required=True,
metadata={
'description': 'The ID of the room containing the message',
'example': '!aBcDeFgHiJkMnO:matrix.example.org',
},
)
user_id = fields.String(
required=True,
attribute='sender',
metadata={
'description': 'ID of the user who sent the message',
'example': '@myuser:matrix.example.org',
},
)
body = fields.String(
required=True,
metadata={
'description': 'Message body',
'example': 'Hello world!',
},
)
format = fields.String(
metadata={
'description': 'Message format',
'example': 'markdown',
},
)
formatted_body = fields.String(
metadata={
'description': 'Formatted body',
'example': '**Hello world!**',
},
)
url = fields.String(
metadata={
'description': 'mxc:// URL if this message contains an attachment',
'example': 'mxc://matrix.example.org/oarGdlpvcwppARPjzNlmlXkD',
},
)
content_type = fields.String(
attribute='mimetype',
metadata={
'description': 'If the message contains an attachment, this field '
'will contain its MIME type',
'example': 'image/jpeg',
},
)
transaction_id = fields.String(
metadata={
'description': 'Set if this message a unique transaction_id associated',
'example': 'mQ8hZR6Dx8I8YDMwONYmBkf7lTgJSMV/ZPqosDNM',
},
)
decrypted = fields.Bool(
metadata={
'description': 'True if the message was encrypted and has been '
'successfully decrypted',
},
)
verified = fields.Bool(
metadata={
'description': 'True if this is an encrypted message coming from a '
'verified source'
},
)
hashes = fields.Dict(
metadata={
'description': 'If the message has been decrypted, this field '
'contains a mapping of its hashes',
'example': {'sha256': 'yoQLQwcURq6/bJp1xQ/uhn9Z2xeA27KhMhPd/mfT8tR'},
},
)
iv = fields.String(
metadata={
'description': 'If the message has been decrypted, this field '
'contains the encryption initial value',
'example': 'NqJMMdijlLvAAAAAAAAAAA',
},
)
key = fields.Dict(
metadata={
'description': 'If the message has been decrypted, this field '
'contains the encryption/decryption key',
'example': {
'alg': 'A256CTR',
'ext': True,
'k': 'u6jjAyNvJoBHE55P5ZfvX49m3oSt9s_L4PSQdprRSJI',
'key_ops': ['encrypt', 'decrypt'],
'kty': 'oct',
},
},
)
timestamp = MillisecondsTimestamp(
required=True,
attribute='server_timestamp',
metadata={
'description': 'When the event was registered on the server',
'example': '2022-07-23T17:20:01.254223',
},
)
class MatrixMessagesResponseSchema(Schema):
messages = fields.Nested(
MatrixMessageSchema(),
many=True,
required=True,
attribute='chunk',
)
start = fields.String(
required=True,
nullable=True,
metadata={
'description': 'Pointer to the first message. It can be used as a '
'``start``/``end`` for another ``get_messages`` query.',
'example': 's10226_143893_619_3648_5951_5_555_7501_0',
},
)
end = fields.String(
required=True,
nullable=True,
metadata={
'description': 'Pointer to the last message. It can be used as a '
'``start``/``end`` for another ``get_messages`` query.',
'example': 't2-10202_143892_626_3663_5949_6_558_7501_0',
},
)
start_time = MillisecondsTimestamp(
required=True,
nullable=True,
metadata={
'description': 'The oldest timestamp of the returned messages',
'example': '2022-07-23T16:20:01.254223',
},
)
end_time = MillisecondsTimestamp(
required=True,
nullable=True,
metadata={
'description': 'The newest timestamp of the returned messages',
'example': '2022-07-23T18:20:01.254223',
},
)

View file

@ -268,5 +268,7 @@ setup(
'ngrok': ['pyngrok'], 'ngrok': ['pyngrok'],
# Support for IRC integration # Support for IRC integration
'irc': ['irc'], 'irc': ['irc'],
# Support for the Matrix integration
'matrix': ['matrix-nio'],
}, },
) )