From 3b554bda267402fd43a9e462eccf4060077f37dc Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 24 Mar 2016 13:19:39 +0000 Subject: Never notify for member events. This fixes https://github.com/vector-im/vector-web/issues/828 --- synapse/push/baserules.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/baserules.py b/synapse/push/baserules.py index 86a2998bcc..792af70eb7 100644 --- a/synapse/push/baserules.py +++ b/synapse/push/baserules.py @@ -160,7 +160,27 @@ BASE_APPEND_OVRRIDE_RULES = [ 'actions': [ 'dont_notify', ] - } + }, + # Will we sometimes want to know about people joining and leaving? + # Perhaps: if so, this could be expanded upon. Seems the most usual case + # is that we don't though. We add this override rule so that even if + # the room rule is set to notify, we don't get notifications about + # join/leave/avatar/displayname events. + # See also: https://matrix.org/jira/browse/SYN-607 + { + 'rule_id': 'global/override/.m.rule.member_event', + 'conditions': [ + { + 'kind': 'event_match', + 'key': 'type', + 'pattern': 'm.room.member', + '_id': '_member', + } + ], + 'actions': [ + 'dont_notify' + ] + }, ] @@ -261,25 +281,6 @@ BASE_APPEND_UNDERRIDE_RULES = [ } ] }, - # This is too simple: https://matrix.org/jira/browse/SYN-607 - # Removing for now - # { - # 'rule_id': 'global/underride/.m.rule.member_event', - # 'conditions': [ - # { - # 'kind': 'event_match', - # 'key': 'type', - # 'pattern': 'm.room.member', - # '_id': '_member', - # } - # ], - # 'actions': [ - # 'notify', { - # 'set_tweak': 'highlight', - # 'value': False - # } - # ] - # }, { 'rule_id': 'global/underride/.m.rule.message', 'conditions': [ -- cgit 1.5.1 From c27c51484a306caf3946f205a933878c563d3d8a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Mar 2016 10:12:31 +0100 Subject: Don't ignore the obey overlay if the rule has an enabled attribute of False Fixes https://github.com/vector-im/vector-web/issues/1244 --- synapse/push/push_rule_evaluator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py index 51f73a5b78..c3c2877629 100644 --- a/synapse/push/push_rule_evaluator.py +++ b/synapse/push/push_rule_evaluator.py @@ -133,8 +133,9 @@ class PushRuleEvaluator: enabled = self.enabled_map.get(r['rule_id'], None) if enabled is not None and not enabled: continue - - if not r.get("enabled", True): + elif enabled is None and not r.get("enabled", True): + # if no override, check enabled on the rule itself + # (may have come from a base rule) continue conditions = r['conditions'] -- cgit 1.5.1 From 2a37467fa1358eb41513893efe44cbd294dca36c Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 1 Apr 2016 16:08:59 +0100 Subject: Use google style doc strings. pycharm supports them so there is no need to use the other format. Might as well convert the existing strings to reduce the risk of people accidentally cargo culting the wrong doc string format. --- setup.cfg | 3 ++ synapse/handlers/_base.py | 27 +++++++----- synapse/handlers/auth.py | 26 +++++++---- synapse/handlers/federation.py | 23 +++++----- synapse/handlers/room_member.py | 48 ++++++++++----------- synapse/handlers/sync.py | 49 +++++++++++++-------- synapse/http/servlet.py | 81 ++++++++++++++++++++++------------- synapse/notifier.py | 15 ++++--- synapse/push/baserules.py | 8 ++-- synapse/rest/client/v2_alpha/sync.py | 79 ++++++++++++++++++---------------- synapse/state.py | 19 ++++---- synapse/storage/event_push_actions.py | 5 ++- synapse/storage/registration.py | 15 ++++--- synapse/storage/state.py | 13 +++--- 14 files changed, 242 insertions(+), 169 deletions(-) (limited to 'synapse/push') diff --git a/setup.cfg b/setup.cfg index f8cc13c840..5ebce1c56b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,3 +17,6 @@ ignore = [flake8] max-line-length = 90 ignore = W503 ; W503 requires that binary operators be at the end, not start, of lines. Erik doesn't like it. + +[pep8] +max-line-length = 90 diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 90eabb6eb7..5601ecea6e 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -41,8 +41,9 @@ class BaseHandler(object): """ Common base class for the event handlers. - :type store: synapse.storage.events.StateStore - :type state_handler: synapse.state.StateHandler + Attributes: + store (synapse.storage.events.StateStore): + state_handler (synapse.state.StateHandler): """ def __init__(self, hs): @@ -65,11 +66,12 @@ class BaseHandler(object): """ Returns dict of user_id -> list of events that user is allowed to see. - :param (str, bool) user_tuples: (user id, is_peeking) for each - user to be checked. is_peeking should be true if: - * the user is not currently a member of the room, and: - * the user has not been a member of the room since the given - events + Args: + user_tuples (str, bool): (user id, is_peeking) for each user to be + checked. is_peeking should be true if: + * the user is not currently a member of the room, and: + * the user has not been a member of the room since the + given events """ forgotten = yield defer.gatherResults([ self.store.who_forgot_in_room( @@ -165,13 +167,16 @@ class BaseHandler(object): """ Check which events a user is allowed to see - :param str user_id: user id to be checked - :param [synapse.events.EventBase] events: list of events to be checked - :param bool is_peeking should be True if: + Args: + user_id(str): user id to be checked + events([synapse.events.EventBase]): list of events to be checked + is_peeking(bool): should be True if: * the user is not currently a member of the room, and: * the user has not been a member of the room since the given events - :rtype [synapse.events.EventBase] + + Returns: + [synapse.events.EventBase] """ types = ( (EventTypes.RoomHistoryVisibility, ""), diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 82d458b424..d5d6faa85f 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -163,9 +163,13 @@ class AuthHandler(BaseHandler): def get_session_id(self, clientdict): """ Gets the session ID for a client given the client dictionary - :param clientdict: The dictionary sent by the client in the request - :return: The string session ID the client sent. If the client did not - send a session ID, returns None. + + Args: + clientdict: The dictionary sent by the client in the request + + Returns: + str|None: The string session ID the client sent. If the client did + not send a session ID, returns None. """ sid = None if clientdict and 'auth' in clientdict: @@ -179,9 +183,11 @@ class AuthHandler(BaseHandler): Store a key-value pair into the sessions data associated with this request. This data is stored server-side and cannot be modified by the client. - :param session_id: (string) The ID of this session as returned from check_auth - :param key: (string) The key to store the data under - :param value: (any) The data to store + + Args: + session_id (string): The ID of this session as returned from check_auth + key (string): The key to store the data under + value (any): The data to store """ sess = self._get_session_info(session_id) sess.setdefault('serverdict', {})[key] = value @@ -190,9 +196,11 @@ class AuthHandler(BaseHandler): def get_session_data(self, session_id, key, default=None): """ Retrieve data stored with set_session_data - :param session_id: (string) The ID of this session as returned from check_auth - :param key: (string) The key to store the data under - :param default: (any) Value to return if the key has not been set + + Args: + session_id (string): The ID of this session as returned from check_auth + key (string): The key to store the data under + default (any): Value to return if the key has not been set """ sess = self._get_session_info(session_id) return sess.setdefault('serverdict', {}).get(key, default) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 4a35344d32..092802b973 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1706,13 +1706,15 @@ class FederationHandler(BaseHandler): def _check_signature(self, event, auth_events): """ Checks that the signature in the event is consistent with its invite. - :param event (Event): The m.room.member event to check - :param auth_events (dict<(event type, state_key), event>) - :raises - AuthError if signature didn't match any keys, or key has been + Args: + event (Event): The m.room.member event to check + auth_events (dict<(event type, state_key), event>): + + Raises: + AuthError: if signature didn't match any keys, or key has been revoked, - SynapseError if a transient error meant a key couldn't be checked + SynapseError: if a transient error meant a key couldn't be checked for revocation. """ signed = event.content["third_party_invite"]["signed"] @@ -1754,12 +1756,13 @@ class FederationHandler(BaseHandler): """ Checks whether public_key has been revoked. - :param public_key (str): base-64 encoded public key. - :param url (str): Key revocation URL. + Args: + public_key (str): base-64 encoded public key. + url (str): Key revocation URL. - :raises - AuthError if they key has been revoked. - SynapseError if a transient error meant a key couldn't be checked + Raises: + AuthError: if they key has been revoked. + SynapseError: if a transient error meant a key couldn't be checked for revocation. """ try: diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 5fdbd3adcc..01f833c371 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -411,7 +411,7 @@ class RoomMemberHandler(BaseHandler): address (str): The third party identifier (e.g. "foo@example.com"). Returns: - (str) the matrix ID of the 3pid, or None if it is not recognized. + str: the matrix ID of the 3pid, or None if it is not recognized. """ try: data = yield self.hs.get_simple_http_client().get_json( @@ -545,29 +545,29 @@ class RoomMemberHandler(BaseHandler): """ Asks an identity server for a third party invite. - :param id_server (str): hostname + optional port for the identity server. - :param medium (str): The literal string "email". - :param address (str): The third party address being invited. - :param room_id (str): The ID of the room to which the user is invited. - :param inviter_user_id (str): The user ID of the inviter. - :param room_alias (str): An alias for the room, for cosmetic - notifications. - :param room_avatar_url (str): The URL of the room's avatar, for cosmetic - notifications. - :param room_join_rules (str): The join rules of the email - (e.g. "public"). - :param room_name (str): The m.room.name of the room. - :param inviter_display_name (str): The current display name of the - inviter. - :param inviter_avatar_url (str): The URL of the inviter's avatar. - - :return: A deferred tuple containing: - token (str): The token which must be signed to prove authenticity. - public_keys ([{"public_key": str, "key_validity_url": str}]): - public_key is a base64-encoded ed25519 public key. - fallback_public_key: One element from public_keys. - display_name (str): A user-friendly name to represent the invited - user. + Args: + id_server (str): hostname + optional port for the identity server. + medium (str): The literal string "email". + address (str): The third party address being invited. + room_id (str): The ID of the room to which the user is invited. + inviter_user_id (str): The user ID of the inviter. + room_alias (str): An alias for the room, for cosmetic notifications. + room_avatar_url (str): The URL of the room's avatar, for cosmetic + notifications. + room_join_rules (str): The join rules of the email (e.g. "public"). + room_name (str): The m.room.name of the room. + inviter_display_name (str): The current display name of the + inviter. + inviter_avatar_url (str): The URL of the inviter's avatar. + + Returns: + A deferred tuple containing: + token (str): The token which must be signed to prove authenticity. + public_keys ([{"public_key": str, "key_validity_url": str}]): + public_key is a base64-encoded ed25519 public key. + fallback_public_key: One element from public_keys. + display_name (str): A user-friendly name to represent the invited + user. """ is_url = "%s%s/_matrix/identity/api/v1/store-invite" % ( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 48ab5707e1..20a0626574 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -671,7 +671,8 @@ class SyncHandler(BaseHandler): def load_filtered_recents(self, room_id, sync_config, now_token, since_token=None, recents=None, newly_joined_room=False): """ - :returns a Deferred TimelineBatch + Returns: + a Deferred TimelineBatch """ with Measure(self.clock, "load_filtered_recents"): filtering_factor = 2 @@ -838,8 +839,11 @@ class SyncHandler(BaseHandler): """ Get the room state after the given event - :param synapse.events.EventBase event: event of interest - :return: A Deferred map from ((type, state_key)->Event) + Args: + event(synapse.events.EventBase): event of interest + + Returns: + A Deferred map from ((type, state_key)->Event) """ state = yield self.store.get_state_for_event(event.event_id) if event.is_state(): @@ -850,9 +854,13 @@ class SyncHandler(BaseHandler): @defer.inlineCallbacks def get_state_at(self, room_id, stream_position): """ Get the room state at a particular stream position - :param str room_id: room for which to get state - :param StreamToken stream_position: point at which to get state - :returns: A Deferred map from ((type, state_key)->Event) + + Args: + room_id(str): room for which to get state + stream_position(StreamToken): point at which to get state + + Returns: + A Deferred map from ((type, state_key)->Event) """ last_events, token = yield self.store.get_recent_events_for_room( room_id, end_token=stream_position.room_key, limit=1, @@ -873,15 +881,18 @@ class SyncHandler(BaseHandler): """ Works out the differnce in state between the start of the timeline and the previous sync. - :param str room_id - :param TimelineBatch batch: The timeline batch for the room that will - be sent to the user. - :param sync_config - :param str since_token: Token of the end of the previous batch. May be None. - :param str now_token: Token of the end of the current batch. - :param bool full_state: Whether to force returning the full state. + Args: + room_id(str): + batch(synapse.handlers.sync.TimelineBatch): The timeline batch for + the room that will be sent to the user. + sync_config(synapse.handlers.sync.SyncConfig): + since_token(str|None): Token of the end of the previous batch. May + be None. + now_token(str): Token of the end of the current batch. + full_state(bool): Whether to force returning the full state. - :returns A new event dictionary + Returns: + A deferred new event dictionary """ # TODO(mjark) Check if the state events were received by the server # after the previous sync, since we need to include those state @@ -953,11 +964,13 @@ class SyncHandler(BaseHandler): Check if the user has just joined the given room (so should be given the full state) - :param sync_config: - :param dict[(str,str), synapse.events.FrozenEvent] state_delta: the - difference in state since the last sync + Args: + sync_config(synapse.handlers.sync.SyncConfig): + state_delta(dict[(str,str), synapse.events.FrozenEvent]): the + difference in state since the last sync - :returns A deferred Tuple (state_delta, limited) + Returns: + A deferred Tuple (state_delta, limited) """ join_event = state_delta.get(( EventTypes.Member, sync_config.user.to_string()), None) diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 1c8bd8666f..e41afeab8e 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -26,14 +26,19 @@ logger = logging.getLogger(__name__) def parse_integer(request, name, default=None, required=False): """Parse an integer parameter from the request string - :param request: the twisted HTTP request. - :param name (str): the name of the query parameter. - :param default: value to use if the parameter is absent, defaults to None. - :param required (bool): whether to raise a 400 SynapseError if the - parameter is absent, defaults to False. - :return: An int value or the default. - :raises - SynapseError if the parameter is absent and required, or if the + Args: + request: the twisted HTTP request. + name (str): the name of the query parameter. + default (int|None): value to use if the parameter is absent, defaults + to None. + required (bool): whether to raise a 400 SynapseError if the + parameter is absent, defaults to False. + + Returns: + int|None: An int value or the default. + + Raises: + SynapseError: if the parameter is absent and required, or if the parameter is present and not an integer. """ if name in request.args: @@ -53,14 +58,19 @@ def parse_integer(request, name, default=None, required=False): def parse_boolean(request, name, default=None, required=False): """Parse a boolean parameter from the request query string - :param request: the twisted HTTP request. - :param name (str): the name of the query parameter. - :param default: value to use if the parameter is absent, defaults to None. - :param required (bool): whether to raise a 400 SynapseError if the - parameter is absent, defaults to False. - :return: A bool value or the default. - :raises - SynapseError if the parameter is absent and required, or if the + Args: + request: the twisted HTTP request. + name (str): the name of the query parameter. + default (bool|None): value to use if the parameter is absent, defaults + to None. + required (bool): whether to raise a 400 SynapseError if the + parameter is absent, defaults to False. + + Returns: + bool|None: A bool value or the default. + + Raises: + SynapseError: if the parameter is absent and required, or if the parameter is present and not one of "true" or "false". """ @@ -88,15 +98,20 @@ def parse_string(request, name, default=None, required=False, allowed_values=None, param_type="string"): """Parse a string parameter from the request query string. - :param request: the twisted HTTP request. - :param name (str): the name of the query parameter. - :param default: value to use if the parameter is absent, defaults to None. - :param required (bool): whether to raise a 400 SynapseError if the - parameter is absent, defaults to False. - :param allowed_values (list): List of allowed values for the string, - or None if any value is allowed, defaults to None - :return: A string value or the default. - :raises + Args: + request: the twisted HTTP request. + name (str): the name of the query parameter. + default (str|None): value to use if the parameter is absent, defaults + to None. + required (bool): whether to raise a 400 SynapseError if the + parameter is absent, defaults to False. + allowed_values (list[str]): List of allowed values for the string, + or None if any value is allowed, defaults to None + + Returns: + str|None: A string value or the default. + + Raises: SynapseError if the parameter is absent and required, or if the parameter is present, must be one of a list of allowed values and is not one of those allowed values. @@ -122,9 +137,13 @@ def parse_string(request, name, default=None, required=False, def parse_json_value_from_request(request): """Parse a JSON value from the body of a twisted HTTP request. - :param request: the twisted HTTP request. - :returns: The JSON value. - :raises + Args: + request: the twisted HTTP request. + + Returns: + The JSON value. + + Raises: SynapseError if the request body couldn't be decoded as JSON. """ try: @@ -143,8 +162,10 @@ def parse_json_value_from_request(request): def parse_json_object_from_request(request): """Parse a JSON object from the body of a twisted HTTP request. - :param request: the twisted HTTP request. - :raises + Args: + request: the twisted HTTP request. + + Raises: SynapseError if the request body couldn't be decoded as JSON or if it wasn't a JSON object. """ diff --git a/synapse/notifier.py b/synapse/notifier.py index f00cd8c588..6af7a8f424 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -503,13 +503,14 @@ class Notifier(object): def wait_for_replication(self, callback, timeout): """Wait for an event to happen. - :param callback: - Gets called whenever an event happens. If this returns a truthy - value then ``wait_for_replication`` returns, otherwise it waits - for another event. - :param int timeout: - How many milliseconds to wait for callback return a truthy value. - :returns: + Args: + callback: Gets called whenever an event happens. If this returns a + truthy value then ``wait_for_replication`` returns, otherwise + it waits for another event. + timeout: How many milliseconds to wait for callback return a truthy + value. + + Returns: A deferred that resolves with the value returned by the callback. """ listener = _NotificationListener(None) diff --git a/synapse/push/baserules.py b/synapse/push/baserules.py index 792af70eb7..6add94beeb 100644 --- a/synapse/push/baserules.py +++ b/synapse/push/baserules.py @@ -19,9 +19,11 @@ import copy def list_with_base_rules(rawrules): """Combine the list of rules set by the user with the default push rules - :param list rawrules: The rules the user has modified or set. - :returns: A new list with the rules set by the user combined with the - defaults. + Args: + rawrules(list): The rules the user has modified or set. + + Returns: + A new list with the rules set by the user combined with the defaults. """ ruleslist = [] diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index c5785d7074..60d3dc4030 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -199,15 +199,17 @@ class SyncRestServlet(RestServlet): """ Encode the joined rooms in a sync result - :param list[synapse.handlers.sync.JoinedSyncResult] rooms: list of sync - results for rooms this user is joined to - :param int time_now: current time - used as a baseline for age - calculations - :param int token_id: ID of the user's auth token - used for namespacing - of transaction IDs - - :return: the joined rooms list, in our response format - :rtype: dict[str, dict[str, object]] + Args: + rooms(list[synapse.handlers.sync.JoinedSyncResult]): list of sync + results for rooms this user is joined to + time_now(int): current time - used as a baseline for age + calculations + token_id(int): ID of the user's auth token - used for namespacing + of transaction IDs + + Returns: + dict[str, dict[str, object]]: the joined rooms list, in our + response format """ joined = {} for room in rooms: @@ -221,15 +223,17 @@ class SyncRestServlet(RestServlet): """ Encode the invited rooms in a sync result - :param list[synapse.handlers.sync.InvitedSyncResult] rooms: list of - sync results for rooms this user is joined to - :param int time_now: current time - used as a baseline for age - calculations - :param int token_id: ID of the user's auth token - used for namespacing + Args: + rooms(list[synapse.handlers.sync.InvitedSyncResult]): list of + sync results for rooms this user is joined to + time_now(int): current time - used as a baseline for age + calculations + token_id(int): ID of the user's auth token - used for namespacing of transaction IDs - :return: the invited rooms list, in our response format - :rtype: dict[str, dict[str, object]] + Returns: + dict[str, dict[str, object]]: the invited rooms list, in our + response format """ invited = {} for room in rooms: @@ -251,15 +255,17 @@ class SyncRestServlet(RestServlet): """ Encode the archived rooms in a sync result - :param list[synapse.handlers.sync.ArchivedSyncResult] rooms: list of - sync results for rooms this user is joined to - :param int time_now: current time - used as a baseline for age - calculations - :param int token_id: ID of the user's auth token - used for namespacing - of transaction IDs - - :return: the invited rooms list, in our response format - :rtype: dict[str, dict[str, object]] + Args: + rooms (list[synapse.handlers.sync.ArchivedSyncResult]): list of + sync results for rooms this user is joined to + time_now(int): current time - used as a baseline for age + calculations + token_id(int): ID of the user's auth token - used for namespacing + of transaction IDs + + Returns: + dict[str, dict[str, object]]: The invited rooms list, in our + response format """ joined = {} for room in rooms: @@ -272,17 +278,18 @@ class SyncRestServlet(RestServlet): @staticmethod def encode_room(room, time_now, token_id, joined=True): """ - :param JoinedSyncResult|ArchivedSyncResult room: sync result for a - single room - :param int time_now: current time - used as a baseline for age - calculations - :param int token_id: ID of the user's auth token - used for namespacing - of transaction IDs - :param joined: True if the user is joined to this room - will mean - we handle ephemeral events - - :return: the room, encoded in our response format - :rtype: dict[str, object] + Args: + room (JoinedSyncResult|ArchivedSyncResult): sync result for a + single room + time_now (int): current time - used as a baseline for age + calculations + token_id (int): ID of the user's auth token - used for namespacing + of transaction IDs + joined (bool): True if the user is joined to this room - will mean + we handle ephemeral events + + Returns: + dict[str, object]: the room, encoded in our response format """ def serialize(event): # TODO(mjark): Respect formatting requirements in the filter. diff --git a/synapse/state.py b/synapse/state.py index 41d32e664a..4a9e148de7 100644 --- a/synapse/state.py +++ b/synapse/state.py @@ -86,7 +86,8 @@ class StateHandler(object): If `event_type` is specified, then the method returns only the one event (or None) with that `event_type` and `state_key`. - :returns map from (type, state_key) to event + Returns: + map from (type, state_key) to event """ event_ids = yield self.store.get_latest_event_ids_in_room(room_id) @@ -176,10 +177,11 @@ class StateHandler(object): """ Given a list of event_ids this method fetches the state at each event, resolves conflicts between them and returns them. - :returns a Deferred tuple of (`state_group`, `state`, `prev_state`). - `state_group` is the name of a state group if one and only one is - involved. `state` is a map from (type, state_key) to event, and - `prev_state` is a list of event ids. + Returns: + a Deferred tuple of (`state_group`, `state`, `prev_state`). + `state_group` is the name of a state group if one and only one is + involved. `state` is a map from (type, state_key) to event, and + `prev_state` is a list of event ids. """ logger.debug("resolve_state_groups event_ids %s", event_ids) @@ -251,9 +253,10 @@ class StateHandler(object): def _resolve_events(self, state_sets, event_type=None, state_key=""): """ - :returns a tuple (new_state, prev_states). new_state is a map - from (type, state_key) to event. prev_states is a list of event_ids. - :rtype: (dict[(str, str), synapse.events.FrozenEvent], list[str]) + Returns + (dict[(str, str), synapse.events.FrozenEvent], list[str]): a tuple + (new_state, prev_states). new_state is a map from (type, state_key) + to event. prev_states is a list of event_ids. """ with Measure(self.clock, "state._resolve_events"): state = {} diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index dc5830450a..3933b6e2c5 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -26,8 +26,9 @@ logger = logging.getLogger(__name__) class EventPushActionsStore(SQLBaseStore): def _set_push_actions_for_event_and_users_txn(self, txn, event, tuples): """ - :param event: the event set actions for - :param tuples: list of tuples of (user_id, actions) + Args: + event: the event set actions for + tuples: list of tuples of (user_id, actions) """ values = [] for uid, actions in tuples: diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index bd4eb88a92..d46a963bb8 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -458,12 +458,15 @@ class RegistrationStore(SQLBaseStore): """ Gets the 3pid's guest access token if exists, else saves access_token. - :param medium (str): Medium of the 3pid. Must be "email". - :param address (str): 3pid address. - :param access_token (str): The access token to persist if none is - already persisted. - :param inviter_user_id (str): User ID of the inviter. - :return (deferred str): Whichever access token is persisted at the end + Args: + medium (str): Medium of the 3pid. Must be "email". + address (str): 3pid address. + access_token (str): The access token to persist if none is + already persisted. + inviter_user_id (str): User ID of the inviter. + + Returns: + deferred str: Whichever access token is persisted at the end of this function call. """ def insert(txn): diff --git a/synapse/storage/state.py b/synapse/storage/state.py index 7fc9a4f264..f84fd0e30a 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -249,11 +249,14 @@ class StateStore(SQLBaseStore): """ Get the state dict corresponding to a particular event - :param str event_id: event whose state should be returned - :param list[(str, str)]|None types: List of (type, state_key) tuples - which are used to filter the state fetched. May be None, which - matches any key - :return: a deferred dict from (type, state_key) -> state_event + Args: + event_id(str): event whose state should be returned + types(list[(str, str)]|None): List of (type, state_key) tuples + which are used to filter the state fetched. May be None, which + matches any key + + Returns: + A deferred dict from (type, state_key) -> state_event """ state_map = yield self.get_state_for_events([event_id], types) defer.returnValue(state_map[event_id]) -- cgit 1.5.1 From 7e2c89a37f3a5261f43b4d472b36219ac41dfb16 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 6 Apr 2016 15:42:15 +0100 Subject: Make pushers use the event_push_actions table instead of listening on an event stream & running the rules again. Sytest passes, but remaining to do: * Make badges work again * Remove old, unused code --- synapse/handlers/_base.py | 8 +- synapse/handlers/federation.py | 8 +- synapse/push/bulk_push_rule_evaluator.py | 25 +++- synapse/push/httppusher.py | 204 +++++++++++++++++++++++------ synapse/push/push_tools.py | 66 ++++++++++ synapse/push/pusher.py | 10 ++ synapse/push/pusherpool.py | 75 ++++++----- synapse/storage/event_push_actions.py | 48 +++++++ synapse/storage/events.py | 12 ++ synapse/storage/pusher.py | 81 ++++++++---- synapse/storage/registration.py | 20 --- synapse/storage/roommember.py | 1 + synapse/storage/schema/delta/31/pushers.py | 75 +++++++++++ 13 files changed, 503 insertions(+), 130 deletions(-) create mode 100644 synapse/push/push_tools.py create mode 100644 synapse/push/pusher.py create mode 100644 synapse/storage/schema/delta/31/pushers.py (limited to 'synapse/push') diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index c77afe7f51..9c92ea01ed 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -21,7 +21,7 @@ from synapse.api.constants import Membership, EventTypes from synapse.types import UserID, RoomAlias, Requester from synapse.push.action_generator import ActionGenerator -from synapse.util.logcontext import PreserveLoggingContext +from synapse.util.logcontext import PreserveLoggingContext, preserve_fn import logging @@ -377,6 +377,12 @@ class BaseHandler(object): event, context=context ) + # this intentionally does not yield: we don't care about the result + # and don't need to wait for it. + preserve_fn(self.hs.get_pusherpool().on_new_notifications)( + event_stream_id, max_stream_id + ) + destinations = set() for k, s in context.current_state.items(): try: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 026ebe52be..fc5e0b0590 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -26,7 +26,7 @@ from synapse.api.errors import ( from synapse.api.constants import EventTypes, Membership, RejectedReason from synapse.events.validator import EventValidator from synapse.util import unwrapFirstError -from synapse.util.logcontext import PreserveLoggingContext +from synapse.util.logcontext import PreserveLoggingContext, preserve_fn from synapse.util.logutils import log_function from synapse.util.async import run_on_reactor from synapse.util.frozenutils import unfreeze @@ -1094,6 +1094,12 @@ class FederationHandler(BaseHandler): context=context, ) + # this intentionally does not yield: we don't care about the result + # and don't need to wait for it. + preserve_fn(self.hs.get_pusherpool().on_new_notifications)( + event_stream_id, max_stream_id + ) + defer.returnValue((context, event_stream_id, max_stream_id)) @defer.inlineCallbacks diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 76d7eb7ce0..7f94591dcb 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -70,11 +70,17 @@ def _get_rules(room_id, user_ids, store): @defer.inlineCallbacks def evaluator_for_room_id(room_id, hs, store): - results = yield store.get_receipts_for_room(room_id, "m.read") - user_ids = [ - row["user_id"] for row in results - if hs.is_mine_id(row["user_id"]) - ] + users_with_pushers = yield store.get_users_with_pushers_in_room(room_id) + receipts = yield store.get_receipts_for_room(room_id, "m.read") + + # any users with pushers must be ours: they have pushers + user_ids = set(users_with_pushers) + for r in receipts: + if hs.is_mine_id(r['user_id']): + user_ids.add(r['user_id']) + + user_ids = list(user_ids) + rules_by_user = yield _get_rules(room_id, user_ids, store) defer.returnValue(BulkPushRuleEvaluator( @@ -101,10 +107,15 @@ class BulkPushRuleEvaluator: def action_for_event_by_user(self, event, handler, current_state): actions_by_user = {} - users_dict = yield self.store.are_guests(self.rules_by_user.keys()) + # None of these users can be peeking since this list of users comes + # from the set of users in the room, so we know for sure they're all + # actually in the room. + user_tuples = [ + (u, False) for u in self.rules_by_user.keys() + ] filtered_by_user = yield handler.filter_events_for_clients( - users_dict.items(), [event], {event.event_id: current_state} + user_tuples, [event], {event.event_id: current_state} ) room_members = yield self.store.get_users_in_room(self.room_id) diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 9be4869360..d695885649 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -13,60 +13,188 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.push import Pusher, PusherConfigException +from synapse.push import PusherConfigException -from twisted.internet import defer +from twisted.internet import defer, reactor import logging +import push_rule_evaluator +import push_tools logger = logging.getLogger(__name__) -class HttpPusher(Pusher): - def __init__(self, _hs, user_id, app_id, - app_display_name, device_display_name, pushkey, pushkey_ts, - data, last_token, last_success, failing_since): - super(HttpPusher, self).__init__( - _hs, - user_id, - app_id, - app_display_name, - device_display_name, - pushkey, - pushkey_ts, - data, - last_token, - last_success, - failing_since +class HttpPusher(object): + INITIAL_BACKOFF_SEC = 1 # in seconds because that's what Twisted takes + MAX_BACKOFF_SEC = 60 * 60 + + # This one's in ms because we compare it against the clock + GIVE_UP_AFTER_MS = 24 * 60 * 60 * 1000 + + def __init__(self, hs, pusherdict): + self.hs = hs + self.store = self.hs.get_datastore() + self.clock = self.hs.get_clock() + self.user_id = pusherdict['user_name'] + self.app_id = pusherdict['app_id'] + self.app_display_name = pusherdict['app_display_name'] + self.device_display_name = pusherdict['device_display_name'] + self.pushkey = pusherdict['pushkey'] + self.pushkey_ts = pusherdict['ts'] + self.data = pusherdict['data'] + self.last_stream_ordering = pusherdict['last_stream_ordering'] + self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC + self.failing_since = pusherdict['failing_since'] + self.timed_call = None + + # This is the highest stream ordering we know it's safe to process. + # When new events arrive, we'll be given a window of new events: we + # should honour this rather than just looking for anything higher + # because of potential out-of-order event serialisation. This starts + # off as None though as we don't know any better. + self.max_stream_ordering = None + + if 'data' not in pusherdict: + raise PusherConfigException( + "No 'data' key for HTTP pusher" + ) + self.data = pusherdict['data'] + + self.name = "%s/%s/%s" % ( + pusherdict['user_name'], + pusherdict['app_id'], + pusherdict['pushkey'], ) - if 'url' not in data: + + if 'url' not in self.data: raise PusherConfigException( "'url' required in data for HTTP pusher" ) - self.url = data['url'] - self.http_client = _hs.get_simple_http_client() + self.url = self.data['url'] + self.http_client = hs.get_simple_http_client() self.data_minus_url = {} self.data_minus_url.update(self.data) del self.data_minus_url['url'] + def on_started(self): + self._process() + + def on_new_notifications(self, min_stream_ordering, max_stream_ordering): + self.max_stream_ordering = max_stream_ordering + self._process() + + def on_timer(self): + self._process() + + def on_stop(self): + if self.timed_call: + self.timed_call.cancel() + @defer.inlineCallbacks - def _build_notification_dict(self, event, tweaks, badge): - # we probably do not want to push for every presence update - # (we may want to be able to set up notifications when specific - # people sign in, but we'd want to only deliver the pertinent ones) - # Actually, presence events will not get this far now because we - # need to filter them out in the main Pusher code. - if 'event_id' not in event: - defer.returnValue(None) + def _process(self): + unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( + self.user_id, self.last_stream_ordering, self.max_stream_ordering + ) + + for push_action in unprocessed: + processed = yield self._process_one(push_action) + if processed: + self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC + self.last_stream_ordering = push_action['stream_ordering'] + self.store.update_pusher_last_stream_ordering_and_success( + self.app_id, self.pushkey, self.user_id, + self.last_stream_ordering, + self.clock.time_msec() + ) + self.failing_since = None + yield self.store.update_pusher_failing_since( + self.app_id, self.pushkey, self.user_id, + self.failing_since + ) + else: + self.failing_since = self.clock.time_msec() + yield self.store.update_pusher_failing_since( + self.app_id, self.pushkey, self.user_id, + self.failing_since + ) + + if ( + self.failing_since and + self.failing_since < + self.clock.time_msec() - HttpPusher.GIVE_UP_AFTER + ): + # we really only give up so that if the URL gets + # fixed, we don't suddenly deliver a load + # of old notifications. + logger.warn("Giving up on a notification to user %s, " + "pushkey %s", + self.user_id, self.pushkey) + self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC + self.last_stream_ordering = push_action['stream_ordering'] + yield self.store.update_pusher_last_stream_ordering( + self.app_id, + self.pushkey, + self.user_id, + self.last_stream_ordering + ) + + self.failing_since = None + yield self.store.update_pusher_failing_since( + self.app_id, + self.pushkey, + self.user_id, + self.failing_since + ) + else: + logger.info("Push failed: delaying for %ds", self.backoff_delay) + self.timed_call = reactor.callLater(self.backoff_delay, self.on_timer) + self.backoff_delay = min(self.backoff_delay, self.MAX_BACKOFF_SEC) + break + + @defer.inlineCallbacks + def _process_one(self, push_action): + if 'notify' not in push_action['actions']: + defer.returnValue(True) - ctx = yield self.get_context_for_event(event) + tweaks = push_rule_evaluator.PushRuleEvaluator.tweaks_for_actions(push_action['actions']) + badge = yield push_tools.get_badge_count(self.hs, self.user_id) + + event = yield self.store.get_event(push_action['event_id'], allow_none=True) + if event is None: + defer.returnValue(True) # It's been redacted + rejected = yield self.dispatch_push(event, tweaks, badge) + if rejected is False: + defer.returnValue(False) + + if isinstance(rejected, list) or isinstance(rejected, tuple): + for pk in rejected: + if pk != self.pushkey: + # for sanity, we only remove the pushkey if it + # was the one we actually sent... + logger.warn( + ("Ignoring rejected pushkey %s because we" + " didn't send it"), pk + ) + else: + logger.info( + "Pushkey %s was rejected: removing", + pk + ) + yield self.hs.get_pusherpool().remove_pusher( + self.app_id, pk, self.user_id + ) + defer.returnValue(True) + + @defer.inlineCallbacks + def _build_notification_dict(self, event, tweaks, badge): + ctx = yield push_tools.get_context_for_event(self.hs, event) d = { 'notification': { - 'id': event['event_id'], - 'room_id': event['room_id'], - 'type': event['type'], - 'sender': event['user_id'], + 'id': event.event_id, + 'room_id': event.room_id, + 'type': event.type, + 'sender': event.user_id, 'counts': { # -- we don't mark messages as read yet so # we have no way of knowing # Just set the badge to 1 until we have read receipts @@ -84,11 +212,11 @@ class HttpPusher(Pusher): ] } } - if event['type'] == 'm.room.member': - d['notification']['membership'] = event['content']['membership'] - d['notification']['user_is_target'] = event['state_key'] == self.user_id + if event.type == 'm.room.member': + d['notification']['membership'] = event.content['membership'] + d['notification']['user_is_target'] = event.state_key == self.user_id if 'content' in event: - d['notification']['content'] = event['content'] + d['notification']['content'] = event.content if len(ctx['aliases']): d['notification']['room_alias'] = ctx['aliases'][0] diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py new file mode 100644 index 0000000000..e1e61e49e8 --- /dev/null +++ b/synapse/push/push_tools.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + + +@defer.inlineCallbacks +def get_badge_count(hs, user_id): + invites, joins = yield defer.gatherResults([ + hs.get_datastore().get_invited_rooms_for_user(user_id), + hs.get_datastore().get_rooms_for_user(user_id), + ], consumeErrors=True) + + my_receipts_by_room = yield hs.get_datastore().get_receipts_for_user( + user_id, "m.read", + ) + + badge = len(invites) + + for r in joins: + if r.room_id in my_receipts_by_room: + last_unread_event_id = my_receipts_by_room[r.room_id] + + notifs = yield ( + hs.get_datastore().get_unread_event_push_actions_by_room_for_user( + r.room_id, user_id, last_unread_event_id + ) + ) + badge += notifs["notify_count"] + defer.returnValue(badge) + + +@defer.inlineCallbacks +def get_context_for_event(hs, ev): + name_aliases = yield hs.get_datastore().get_room_name_and_aliases( + ev.room_id + ) + + ctx = {'aliases': name_aliases[1]} + if name_aliases[0] is not None: + ctx['name'] = name_aliases[0] + + their_member_events_for_room = yield hs.get_datastore().get_current_state( + room_id=ev.room_id, + event_type='m.room.member', + state_key=ev.user_id + ) + for mev in their_member_events_for_room: + if mev.content['membership'] == 'join' and 'displayname' in mev.content: + dn = mev.content['displayname'] + if dn is not None: + ctx['sender_display_name'] = dn + + defer.returnValue(ctx) diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py new file mode 100644 index 0000000000..4960837504 --- /dev/null +++ b/synapse/push/pusher.py @@ -0,0 +1,10 @@ +from httppusher import HttpPusher + +PUSHER_TYPES = { + 'http': HttpPusher +} + + +def create_pusher(hs, pusherdict): + if pusherdict['kind'] in PUSHER_TYPES: + return PUSHER_TYPES[pusherdict['kind']](hs, pusherdict) diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 0b463c6fdb..b67ad455ea 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -16,9 +16,10 @@ from twisted.internet import defer -from .httppusher import HttpPusher +import pusher from synapse.push import PusherConfigException from synapse.util.logcontext import preserve_fn +from synapse.util.async import run_on_reactor import logging @@ -48,7 +49,7 @@ class PusherPool: # will then get pulled out of the database, # recreated, added and started: this means we have only one # code path adding pushers. - self._create_pusher({ + pusher.create_pusher(self.hs, { "user_name": user_id, "kind": kind, "app_id": app_id, @@ -58,10 +59,18 @@ class PusherPool: "ts": time_now_msec, "lang": lang, "data": data, - "last_token": None, + "last_stream_ordering": None, "last_success": None, "failing_since": None }) + + # create the pusher setting last_stream_ordering to the current maximum + # stream ordering in event_push_actions, so it will process + # pushes from this point onwards. + last_stream_ordering = ( + yield self.store.get_latest_push_action_stream_ordering() + ) + yield self.store.add_pusher( user_id=user_id, access_token=access_token, @@ -73,6 +82,7 @@ class PusherPool: pushkey_ts=time_now_msec, lang=lang, data=data, + last_stream_ordering=last_stream_ordering, profile_tag=profile_tag, ) yield self._refresh_pusher(app_id, pushkey, user_id) @@ -106,26 +116,19 @@ class PusherPool: ) yield self.remove_pusher(p['app_id'], p['pushkey'], p['user_name']) - def _create_pusher(self, pusherdict): - if pusherdict['kind'] == 'http': - return HttpPusher( - self.hs, - user_id=pusherdict['user_name'], - app_id=pusherdict['app_id'], - app_display_name=pusherdict['app_display_name'], - device_display_name=pusherdict['device_display_name'], - pushkey=pusherdict['pushkey'], - pushkey_ts=pusherdict['ts'], - data=pusherdict['data'], - last_token=pusherdict['last_token'], - last_success=pusherdict['last_success'], - failing_since=pusherdict['failing_since'] - ) - else: - raise PusherConfigException( - "Unknown pusher type '%s' for user %s" % - (pusherdict['kind'], pusherdict['user_name']) + @defer.inlineCallbacks + def on_new_notifications(self, min_stream_id, max_stream_id): + yield run_on_reactor() + try: + users_affected = yield self.store.get_push_action_users_in_range( + min_stream_id, max_stream_id ) + for u in users_affected: + if u in self.pushers: + for p in self.pushers[u].values(): + p.on_new_notifications(min_stream_id, max_stream_id) + except: + logger.exception("Exception in pusher on_new_notifications") @defer.inlineCallbacks def _refresh_pusher(self, app_id, pushkey, user_id): @@ -146,30 +149,34 @@ class PusherPool: logger.info("Starting %d pushers", len(pushers)) for pusherdict in pushers: try: - p = self._create_pusher(pusherdict) + p = pusher.create_pusher(self.hs, pusherdict) except PusherConfigException: logger.exception("Couldn't start a pusher: caught PusherConfigException") continue if p: - fullid = "%s:%s:%s" % ( + appid_pushkey = "%s:%s" % ( pusherdict['app_id'], pusherdict['pushkey'], - pusherdict['user_name'] ) - if fullid in self.pushers: - self.pushers[fullid].stop() - self.pushers[fullid] = p - preserve_fn(p.start)() + byuser = self.pushers.setdefault(pusherdict['user_name'], {}) + + if appid_pushkey in byuser: + byuser[appid_pushkey].on_stop() + byuser[appid_pushkey] = p + preserve_fn(p.on_started)() logger.info("Started pushers") @defer.inlineCallbacks def remove_pusher(self, app_id, pushkey, user_id): - fullid = "%s:%s:%s" % (app_id, pushkey, user_id) - if fullid in self.pushers: - logger.info("Stopping pusher %s", fullid) - self.pushers[fullid].stop() - del self.pushers[fullid] + appid_pushkey = "%s:%s" % (app_id, pushkey) + + byuser = self.pushers.get(user_id, {}) + + if appid_pushkey in byuser: + logger.info("Stopping pusher %s / %s", user_id, appid_pushkey) + byuser[appid_pushkey].on_stop() + del byuser[appid_pushkey] yield self.store.delete_pusher_by_app_id_pushkey_user_id( app_id, pushkey, user_id ) diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index 3933b6e2c5..5f61743e34 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -100,6 +100,54 @@ class EventPushActionsStore(SQLBaseStore): ) defer.returnValue(ret) + @defer.inlineCallbacks + def get_push_action_users_in_range(self, min_stream_ordering, max_stream_ordering): + def f(txn): + sql = ( + "SELECT DISTINCT(user_id) FROM event_push_actions WHERE" + " stream_ordering >= ? AND stream_ordering >= ?" + ) + txn.execute(sql, (min_stream_ordering, max_stream_ordering)) + return [r[0] for r in txn.fetchall()] + ret = yield self.runInteraction("get_push_action_users_in_range", f) + defer.returnValue(ret) + + @defer.inlineCallbacks + def get_unread_push_actions_for_user_in_range(self, user_id, + min_stream_ordering, + max_stream_ordering=None): + def f(txn): + sql = ( + "SELECT event_id, stream_ordering, actions" + " FROM event_push_actions" + " WHERE user_id = ? AND stream_ordering > ?" + ) + args = [user_id, min_stream_ordering] + if max_stream_ordering is not None: + sql += " AND stream_ordering <= ?" + args.append(max_stream_ordering) + sql += " ORDER BY stream_ordering ASC" + txn.execute(sql, args) + return txn.fetchall() + ret = yield self.runInteraction("get_unread_push_actions_for_user_in_range", f) + defer.returnValue([ + { + "event_id": row[0], + "stream_ordering": row[1], + "actions": json.loads(row[2]), + } for row in ret + ]) + + @defer.inlineCallbacks + def get_latest_push_action_stream_ordering(self): + def f(txn): + txn.execute("SELECT MAX(stream_ordering) FROM event_push_actions") + return txn.fetchone() + result = yield self.runInteraction( + "get_latest_push_action_stream_ordering", f + ) + defer.returnValue(result[0] or 0) + def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): # Sad that we have to blow away the cache for the whole room here txn.call_after( diff --git a/synapse/storage/events.py b/synapse/storage/events.py index 5d299a1132..ceae8715ce 100644 --- a/synapse/storage/events.py +++ b/synapse/storage/events.py @@ -61,6 +61,17 @@ class EventsStore(SQLBaseStore): @defer.inlineCallbacks def persist_events(self, events_and_contexts, backfilled=False): + """ + Write events to the database + Args: + events_and_contexts: list of tuples of (event, context) + backfilled: ? + + Returns: Tuple of stream_orderings where the first is the minimum and + last is the maximum stream ordering assigned to the events when + persisting. + + """ if not events_and_contexts: return @@ -191,6 +202,7 @@ class EventsStore(SQLBaseStore): txn.call_after(self._get_current_state_for_key.invalidate_all) txn.call_after(self.get_rooms_for_user.invalidate_all) txn.call_after(self.get_users_in_room.invalidate, (event.room_id,)) + txn.call_after(self.get_users_with_pushers_in_room.invalidate, (event.room_id,)) txn.call_after(self.get_joined_hosts_for_room.invalidate, (event.room_id,)) txn.call_after(self.get_room_name_and_aliases.invalidate, (event.room_id,)) diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index d1669c778a..f7886dd1bb 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -18,6 +18,8 @@ from twisted.internet import defer from canonicaljson import encode_canonical_json +from synapse.util.caches.descriptors import cachedInlineCallbacks + import logging import simplejson as json import types @@ -107,31 +109,46 @@ class PusherStore(SQLBaseStore): "get_all_updated_pushers", get_all_updated_pushers_txn ) + @cachedInlineCallbacks(num_args=1) + def get_users_with_pushers_in_room(self, room_id): + users = yield self.get_users_in_room(room_id) + + result = yield self._simple_select_many_batch( + 'pushers', 'user_name', users, ['user_name'] + ) + + defer.returnValue([r['user_name'] for r in result]) + @defer.inlineCallbacks def add_pusher(self, user_id, access_token, kind, app_id, app_display_name, device_display_name, - pushkey, pushkey_ts, lang, data, profile_tag=""): - with self._pushers_id_gen.get_next() as stream_id: - yield self._simple_upsert( - "pushers", - dict( - app_id=app_id, - pushkey=pushkey, - user_name=user_id, - ), - dict( - access_token=access_token, - kind=kind, - app_display_name=app_display_name, - device_display_name=device_display_name, - ts=pushkey_ts, - lang=lang, - data=encode_canonical_json(data), - profile_tag=profile_tag, - id=stream_id, - ), - desc="add_pusher", - ) + pushkey, pushkey_ts, lang, data, last_stream_ordering, + profile_tag=""): + def f(txn): + txn.call_after(self.get_users_with_pushers_in_room.invalidate_all) + with self._pushers_id_gen.get_next() as stream_id: + return self._simple_upsert_txn( + txn, + "pushers", + dict( + app_id=app_id, + pushkey=pushkey, + user_name=user_id, + ), + dict( + access_token=access_token, + kind=kind, + app_display_name=app_display_name, + device_display_name=device_display_name, + ts=pushkey_ts, + lang=lang, + data=encode_canonical_json(data), + last_stream_ordering=last_stream_ordering, + profile_tag=profile_tag, + id=stream_id, + ), + ) + defer.returnValue((yield self.runInteraction("add_pusher", f))) @defer.inlineCallbacks def delete_pusher_by_app_id_pushkey_user_id(self, app_id, pushkey, user_id): @@ -153,22 +170,28 @@ class PusherStore(SQLBaseStore): ) @defer.inlineCallbacks - def update_pusher_last_token(self, app_id, pushkey, user_id, last_token): + def update_pusher_last_stream_ordering(self, app_id, pushkey, user_id, + last_stream_ordering): yield self._simple_update_one( "pushers", {'app_id': app_id, 'pushkey': pushkey, 'user_name': user_id}, - {'last_token': last_token}, - desc="update_pusher_last_token", + {'last_stream_ordering': last_stream_ordering}, + desc="update_pusher_last_stream_ordering", ) @defer.inlineCallbacks - def update_pusher_last_token_and_success(self, app_id, pushkey, user_id, - last_token, last_success): + def update_pusher_last_stream_ordering_and_success(self, app_id, pushkey, + user_id, + last_stream_ordering, + last_success): yield self._simple_update_one( "pushers", {'app_id': app_id, 'pushkey': pushkey, 'user_name': user_id}, - {'last_token': last_token, 'last_success': last_success}, - desc="update_pusher_last_token_and_success", + { + 'last_stream_ordering': last_stream_ordering, + 'last_success': last_success + }, + desc="update_pusher_last_stream_ordering_and_success", ) @defer.inlineCallbacks diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index d46a963bb8..701dd2f656 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -319,26 +319,6 @@ class RegistrationStore(SQLBaseStore): defer.returnValue(res if res else False) - @cachedList(cache=is_guest.cache, list_name="user_ids", num_args=1, - inlineCallbacks=True) - def are_guests(self, user_ids): - sql = "SELECT name, is_guest FROM users WHERE name IN (%s)" % ( - ",".join("?" for _ in user_ids), - ) - - rows = yield self._execute( - "are_guests", self.cursor_to_dict, sql, *user_ids - ) - - result = {user_id: False for user_id in user_ids} - - result.update({ - row["name"]: bool(row["is_guest"]) - for row in rows - }) - - defer.returnValue(result) - def _query_for_auth(self, txn, token): sql = ( "SELECT users.name, users.is_guest, access_tokens.id as token_id" diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 66e7a40e3c..22a690aa8d 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -58,6 +58,7 @@ class RoomMemberStore(SQLBaseStore): txn.call_after(self.get_rooms_for_user.invalidate, (event.state_key,)) txn.call_after(self.get_joined_hosts_for_room.invalidate, (event.room_id,)) txn.call_after(self.get_users_in_room.invalidate, (event.room_id,)) + txn.call_after(self.get_users_with_pushers_in_room.invalidate, (event.room_id,)) txn.call_after( self._membership_stream_cache.entity_has_changed, event.state_key, event.internal_metadata.stream_ordering diff --git a/synapse/storage/schema/delta/31/pushers.py b/synapse/storage/schema/delta/31/pushers.py new file mode 100644 index 0000000000..7e0e385fb5 --- /dev/null +++ b/synapse/storage/schema/delta/31/pushers.py @@ -0,0 +1,75 @@ +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Change the last_token to last_stream_ordering now that pushers no longer +# listen on an event stream but instead select out of the event_push_actions +# table. + + +import logging + +logger = logging.getLogger(__name__) + + +def token_to_stream_ordering(token): + return int(token[1:].split('_')[0]) + + +def run_upgrade(cur, database_engine, *args, **kwargs): + logger.info("Porting pushers table, delta 31...") + cur.execute(""" + CREATE TABLE IF NOT EXISTS pushers2 ( + id BIGINT PRIMARY KEY, + user_name TEXT NOT NULL, + access_token BIGINT DEFAULT NULL, + profile_tag VARCHAR(32) NOT NULL, + kind VARCHAR(8) NOT NULL, + app_id VARCHAR(64) NOT NULL, + app_display_name VARCHAR(64) NOT NULL, + device_display_name VARCHAR(128) NOT NULL, + pushkey TEXT NOT NULL, + ts BIGINT NOT NULL, + lang VARCHAR(8), + data TEXT, + last_stream_ordering INTEGER, + last_success BIGINT, + failing_since BIGINT, + UNIQUE (app_id, pushkey, user_name) + ) + """) + cur.execute("""SELECT + id, user_name, access_token, profile_tag, kind, + app_id, app_display_name, device_display_name, + pushkey, ts, lang, data, last_token, last_success, + failing_since + FROM pushers + """) + count = 0 + for row in cur.fetchall(): + row = list(row) + row[12] = token_to_stream_ordering(row[12]) + cur.execute(database_engine.convert_param_style(""" + INSERT into pushers2 ( + id, user_name, access_token, profile_tag, kind, + app_id, app_display_name, device_display_name, + pushkey, ts, lang, data, last_stream_ordering, last_success, + failing_since + ) values (%s)""" % (','.join(['?' for _ in range(len(row))]))), + row + ) + count += 1 + cur.execute("DROP TABLE pushers") + cur.execute("ALTER TABLE pushers2 RENAME TO pushers") + logger.info("Moved %d pushers to new table", count) -- cgit 1.5.1 From 92e3071623c34350bf072bb77e089d5d6d5f41c2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 15:39:53 +0100 Subject: Send badge count pushes. Also fix bugs with retrying. --- synapse/handlers/receipts.py | 21 +++++++++++++++++---- synapse/push/httppusher.py | 45 ++++++++++++++++++++++++++++---------------- synapse/push/pusherpool.py | 20 +++++++++++++++++++- synapse/storage/receipts.py | 9 ++++++--- 4 files changed, 71 insertions(+), 24 deletions(-) (limited to 'synapse/push') diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 935c339707..26b0368080 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -80,6 +80,9 @@ class ReceiptsHandler(BaseHandler): def _handle_new_receipts(self, receipts): """Takes a list of receipts, stores them and informs the notifier. """ + min_batch_id = None + max_batch_id = None + for receipt in receipts: room_id = receipt["room_id"] receipt_type = receipt["receipt_type"] @@ -97,10 +100,20 @@ class ReceiptsHandler(BaseHandler): stream_id, max_persisted_id = res - with PreserveLoggingContext(): - self.notifier.on_new_event( - "receipt_key", max_persisted_id, rooms=[room_id] - ) + if min_batch_id is None or stream_id < min_batch_id: + min_batch_id = stream_id + if max_batch_id is None or max_persisted_id > max_batch_id: + max_batch_id = max_persisted_id + + affected_room_ids = list(set([r["room_id"] for r in receipts])) + + with PreserveLoggingContext(): + self.notifier.on_new_event( + "receipt_key", max_batch_id, rooms=affected_room_ids + ) + self.hs.get_pusherpool().on_new_receipts( + min_batch_id, max_batch_id, affected_room_ids + ) defer.returnValue(True) diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index d695885649..0d5450bc01 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -76,15 +76,25 @@ class HttpPusher(object): self.data_minus_url.update(self.data) del self.data_minus_url['url'] + @defer.inlineCallbacks def on_started(self): - self._process() + yield self._process() + @defer.inlineCallbacks def on_new_notifications(self, min_stream_ordering, max_stream_ordering): self.max_stream_ordering = max_stream_ordering - self._process() + yield self._process() + + @defer.inlineCallbacks + def on_new_receipts(self, min_stream_id, max_stream_id): + # We could check the receipts are actually m.read receipts here, + # but currently that's the only type of receipt anyway... + badge = yield push_tools.get_badge_count(self.hs, self.user_id) + yield self.send_badge(badge) + @defer.inlineCallbacks def on_timer(self): - self._process() + yield self._process() def on_stop(self): if self.timed_call: @@ -106,22 +116,24 @@ class HttpPusher(object): self.last_stream_ordering, self.clock.time_msec() ) - self.failing_since = None - yield self.store.update_pusher_failing_since( - self.app_id, self.pushkey, self.user_id, - self.failing_since - ) + if self.failing_since: + self.failing_since = None + yield self.store.update_pusher_failing_since( + self.app_id, self.pushkey, self.user_id, + self.failing_since + ) else: - self.failing_since = self.clock.time_msec() - yield self.store.update_pusher_failing_since( - self.app_id, self.pushkey, self.user_id, - self.failing_since - ) + if not self.failing_since: + self.failing_since = self.clock.time_msec() + yield self.store.update_pusher_failing_since( + self.app_id, self.pushkey, self.user_id, + self.failing_since + ) if ( self.failing_since and self.failing_since < - self.clock.time_msec() - HttpPusher.GIVE_UP_AFTER + self.clock.time_msec() - HttpPusher.GIVE_UP_AFTER_MS ): # we really only give up so that if the URL gets # fixed, we don't suddenly deliver a load @@ -148,7 +160,7 @@ class HttpPusher(object): else: logger.info("Push failed: delaying for %ds", self.backoff_delay) self.timed_call = reactor.callLater(self.backoff_delay, self.on_timer) - self.backoff_delay = min(self.backoff_delay, self.MAX_BACKOFF_SEC) + self.backoff_delay = min(self.backoff_delay * 2, self.MAX_BACKOFF_SEC) break @defer.inlineCallbacks @@ -191,7 +203,8 @@ class HttpPusher(object): d = { 'notification': { - 'id': event.event_id, + 'id': event.event_id, # deprecated: remove soon + 'event_id': event.event_id, 'room_id': event.room_id, 'type': event.type, 'sender': event.user_id, diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index b67ad455ea..7b1ce81e9a 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -126,10 +126,28 @@ class PusherPool: for u in users_affected: if u in self.pushers: for p in self.pushers[u].values(): - p.on_new_notifications(min_stream_id, max_stream_id) + yield p.on_new_notifications(min_stream_id, max_stream_id) except: logger.exception("Exception in pusher on_new_notifications") + @defer.inlineCallbacks + def on_new_receipts(self, min_stream_id, max_stream_id, affected_room_ids): + yield run_on_reactor() + try: + # Need to subtract 1 from the minimum because the lower bound here + # is not inclusive + updated_receipts = yield self.store.get_all_updated_receipts( + min_stream_id - 1, max_stream_id + ) + # This returns a tuple, user_id is at index 3 + users_affected = set([r[3] for r in updated_receipts]) + for u in users_affected: + if u in self.pushers: + for p in self.pushers[u].values(): + yield p.on_new_receipts(min_stream_id, max_stream_id) + except: + logger.exception("Exception in pusher on_new_receipts") + @defer.inlineCallbacks def _refresh_pusher(self, app_id, pushkey, user_id): resultlist = yield self.store.get_pushers_by_app_id_and_pushkey( diff --git a/synapse/storage/receipts.py b/synapse/storage/receipts.py index 4befebc8e2..59d1ac0314 100644 --- a/synapse/storage/receipts.py +++ b/synapse/storage/receipts.py @@ -390,16 +390,19 @@ class ReceiptsStore(SQLBaseStore): } ) - def get_all_updated_receipts(self, last_id, current_id, limit): + def get_all_updated_receipts(self, last_id, current_id, limit=None): def get_all_updated_receipts_txn(txn): sql = ( "SELECT stream_id, room_id, receipt_type, user_id, event_id, data" " FROM receipts_linearized" " WHERE ? < stream_id AND stream_id <= ?" " ORDER BY stream_id ASC" - " LIMIT ?" ) - txn.execute(sql, (last_id, current_id, limit)) + args = [last_id, current_id] + if limit is not None: + sql += " LIMIT ?" + args.append(limit) + txn.execute(sql, args) return txn.fetchall() return self.runInteraction( -- cgit 1.5.1 From d549fdfa22f6927479d2a185f7420cadbfbf5607 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 16:31:38 +0100 Subject: Remove code that's now been obsoleted or moved elsewhere --- synapse/push/__init__.py | 327 ------------------------------------ synapse/push/httppusher.py | 2 +- synapse/push/push_rule_evaluator.py | 134 +-------------- 3 files changed, 9 insertions(+), 454 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/__init__.py b/synapse/push/__init__.py index 296c4447ec..edf45dc599 100644 --- a/synapse/push/__init__.py +++ b/synapse/push/__init__.py @@ -13,333 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - -from synapse.streams.config import PaginationConfig -from synapse.types import StreamToken -from synapse.util.logcontext import LoggingContext -from synapse.util.metrics import Measure - -import synapse.util.async -from .push_rule_evaluator import evaluator_for_user_id - -import logging -import random - -logger = logging.getLogger(__name__) - - -_NEXT_ID = 1 - - -def _get_next_id(): - global _NEXT_ID - _id = _NEXT_ID - _NEXT_ID += 1 - return _id - - -# Pushers could now be moved to pull out of the event_push_actions table instead -# of listening on the event stream: this would avoid them having to run the -# rules again. -class Pusher(object): - INITIAL_BACKOFF = 1000 - MAX_BACKOFF = 60 * 60 * 1000 - GIVE_UP_AFTER = 24 * 60 * 60 * 1000 - - def __init__(self, _hs, user_id, app_id, - app_display_name, device_display_name, pushkey, pushkey_ts, - data, last_token, last_success, failing_since): - self.hs = _hs - self.evStreamHandler = self.hs.get_handlers().event_stream_handler - self.store = self.hs.get_datastore() - self.clock = self.hs.get_clock() - self.user_id = user_id - self.app_id = app_id - self.app_display_name = app_display_name - self.device_display_name = device_display_name - self.pushkey = pushkey - self.pushkey_ts = pushkey_ts - self.data = data - self.last_token = last_token - self.last_success = last_success # not actually used - self.backoff_delay = Pusher.INITIAL_BACKOFF - self.failing_since = failing_since - self.alive = True - self.badge = None - - self.name = "Pusher-%d" % (_get_next_id(),) - - # The last value of last_active_time that we saw - self.last_last_active_time = 0 - self.has_unread = True - - @defer.inlineCallbacks - def get_context_for_event(self, ev): - name_aliases = yield self.store.get_room_name_and_aliases( - ev['room_id'] - ) - - ctx = {'aliases': name_aliases[1]} - if name_aliases[0] is not None: - ctx['name'] = name_aliases[0] - - their_member_events_for_room = yield self.store.get_current_state( - room_id=ev['room_id'], - event_type='m.room.member', - state_key=ev['user_id'] - ) - for mev in their_member_events_for_room: - if mev.content['membership'] == 'join' and 'displayname' in mev.content: - dn = mev.content['displayname'] - if dn is not None: - ctx['sender_display_name'] = dn - - defer.returnValue(ctx) - - @defer.inlineCallbacks - def start(self): - with LoggingContext(self.name): - if not self.last_token: - # First-time setup: get a token to start from (we can't - # just start from no token, ie. 'now' - # because we need the result to be reproduceable in case - # we fail to dispatch the push) - config = PaginationConfig(from_token=None, limit='1') - chunk = yield self.evStreamHandler.get_stream( - self.user_id, config, timeout=0, affect_presence=False - ) - self.last_token = chunk['end'] - yield self.store.update_pusher_last_token( - self.app_id, self.pushkey, self.user_id, self.last_token - ) - logger.info("New pusher %s for user %s starting from token %s", - self.pushkey, self.user_id, self.last_token) - - else: - logger.info( - "Old pusher %s for user %s starting", - self.pushkey, self.user_id, - ) - - wait = 0 - while self.alive: - try: - if wait > 0: - yield synapse.util.async.sleep(wait) - with Measure(self.clock, "push"): - yield self.get_and_dispatch() - wait = 0 - except: - if wait == 0: - wait = 1 - else: - wait = min(wait * 2, 1800) - logger.exception( - "Exception in pusher loop for pushkey %s. Pausing for %ds", - self.pushkey, wait - ) - - @defer.inlineCallbacks - def get_and_dispatch(self): - from_tok = StreamToken.from_string(self.last_token) - config = PaginationConfig(from_token=from_tok, limit='1') - timeout = (300 + random.randint(-60, 60)) * 1000 - chunk = yield self.evStreamHandler.get_stream( - self.user_id, config, timeout=timeout, affect_presence=False, - only_keys=("room", "receipt",), - ) - - # limiting to 1 may get 1 event plus 1 presence event, so - # pick out the actual event - single_event = None - read_receipt = None - for c in chunk['chunk']: - if 'event_id' in c: # Hmmm... - single_event = c - elif c['type'] == 'm.receipt': - read_receipt = c - - have_updated_badge = False - if read_receipt: - for receipt_part in read_receipt['content'].values(): - if 'm.read' in receipt_part: - if self.user_id in receipt_part['m.read'].keys(): - have_updated_badge = True - - if not single_event: - if have_updated_badge: - yield self.update_badge() - self.last_token = chunk['end'] - yield self.store.update_pusher_last_token( - self.app_id, - self.pushkey, - self.user_id, - self.last_token - ) - return - - if not self.alive: - return - - processed = False - - rule_evaluator = yield \ - evaluator_for_user_id( - self.user_id, single_event['room_id'], self.store - ) - - actions = yield rule_evaluator.actions_for_event(single_event) - tweaks = rule_evaluator.tweaks_for_actions(actions) - - if 'notify' in actions: - self.badge = yield self._get_badge_count() - rejected = yield self.dispatch_push(single_event, tweaks, self.badge) - self.has_unread = True - if isinstance(rejected, list) or isinstance(rejected, tuple): - processed = True - for pk in rejected: - if pk != self.pushkey: - # for sanity, we only remove the pushkey if it - # was the one we actually sent... - logger.warn( - ("Ignoring rejected pushkey %s because we" - " didn't send it"), pk - ) - else: - logger.info( - "Pushkey %s was rejected: removing", - pk - ) - yield self.hs.get_pusherpool().remove_pusher( - self.app_id, pk, self.user_id - ) - else: - if have_updated_badge: - yield self.update_badge() - processed = True - - if not self.alive: - return - - if processed: - self.backoff_delay = Pusher.INITIAL_BACKOFF - self.last_token = chunk['end'] - yield self.store.update_pusher_last_token_and_success( - self.app_id, - self.pushkey, - self.user_id, - self.last_token, - self.clock.time_msec() - ) - if self.failing_since: - self.failing_since = None - yield self.store.update_pusher_failing_since( - self.app_id, - self.pushkey, - self.user_id, - self.failing_since) - else: - if not self.failing_since: - self.failing_since = self.clock.time_msec() - yield self.store.update_pusher_failing_since( - self.app_id, - self.pushkey, - self.user_id, - self.failing_since - ) - - if (self.failing_since and - self.failing_since < - self.clock.time_msec() - Pusher.GIVE_UP_AFTER): - # we really only give up so that if the URL gets - # fixed, we don't suddenly deliver a load - # of old notifications. - logger.warn("Giving up on a notification to user %s, " - "pushkey %s", - self.user_id, self.pushkey) - self.backoff_delay = Pusher.INITIAL_BACKOFF - self.last_token = chunk['end'] - yield self.store.update_pusher_last_token( - self.app_id, - self.pushkey, - self.user_id, - self.last_token - ) - - self.failing_since = None - yield self.store.update_pusher_failing_since( - self.app_id, - self.pushkey, - self.user_id, - self.failing_since - ) - else: - logger.warn("Failed to dispatch push for user %s " - "(failing for %dms)." - "Trying again in %dms", - self.user_id, - self.clock.time_msec() - self.failing_since, - self.backoff_delay) - yield synapse.util.async.sleep(self.backoff_delay / 1000.0) - self.backoff_delay *= 2 - if self.backoff_delay > Pusher.MAX_BACKOFF: - self.backoff_delay = Pusher.MAX_BACKOFF - - def stop(self): - self.alive = False - - def dispatch_push(self, p, tweaks, badge): - """ - Overridden by implementing classes to actually deliver the notification - Args: - p: The event to notify for as a single event from the event stream - Returns: If the notification was delivered, an array containing any - pushkeys that were rejected by the push gateway. - False if the notification could not be delivered (ie. - should be retried). - """ - pass - - @defer.inlineCallbacks - def update_badge(self): - new_badge = yield self._get_badge_count() - if self.badge != new_badge: - self.badge = new_badge - yield self.send_badge(self.badge) - - def send_badge(self, badge): - """ - Overridden by implementing classes to send an updated badge count - """ - pass - - @defer.inlineCallbacks - def _get_badge_count(self): - invites, joins = yield defer.gatherResults([ - self.store.get_invited_rooms_for_user(self.user_id), - self.store.get_rooms_for_user(self.user_id), - ], consumeErrors=True) - - my_receipts_by_room = yield self.store.get_receipts_for_user( - self.user_id, - "m.read", - ) - - badge = len(invites) - - for r in joins: - if r.room_id in my_receipts_by_room: - last_unread_event_id = my_receipts_by_room[r.room_id] - - notifs = yield ( - self.store.get_unread_event_push_actions_by_room_for_user( - r.room_id, self.user_id, last_unread_event_id - ) - ) - badge += notifs["notify_count"] - defer.returnValue(badge) - class PusherConfigException(Exception): def __init__(self, msg): diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 0d5450bc01..cc030a57a0 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -168,7 +168,7 @@ class HttpPusher(object): if 'notify' not in push_action['actions']: defer.returnValue(True) - tweaks = push_rule_evaluator.PushRuleEvaluator.tweaks_for_actions(push_action['actions']) + tweaks = push_rule_evaluator.tweaks_for_actions(push_action['actions']) badge = yield push_tools.get_badge_count(self.hs, self.user_id) event = yield self.store.get_event(push_action['event_id'], allow_none=True) diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py index c3c2877629..4db76f18bd 100644 --- a/synapse/push/push_rule_evaluator.py +++ b/synapse/push/push_rule_evaluator.py @@ -13,12 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - -from .baserules import list_with_base_rules - import logging -import simplejson as json import re from synapse.types import UserID @@ -32,22 +27,6 @@ IS_GLOB = re.compile(r'[\?\*\[\]]') INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$") -@defer.inlineCallbacks -def evaluator_for_user_id(user_id, room_id, store): - rawrules = yield store.get_push_rules_for_user(user_id) - enabled_map = yield store.get_push_rules_enabled_for_user(user_id) - our_member_event = yield store.get_current_state( - room_id=room_id, - event_type='m.room.member', - state_key=user_id, - ) - - defer.returnValue(PushRuleEvaluator( - user_id, rawrules, enabled_map, - room_id, our_member_event, store - )) - - def _room_member_count(ev, condition, room_member_count): if 'is' not in condition: return False @@ -74,111 +53,14 @@ def _room_member_count(ev, condition, room_member_count): return False -class PushRuleEvaluator: - DEFAULT_ACTIONS = [] - - def __init__(self, user_id, raw_rules, enabled_map, room_id, - our_member_event, store): - self.user_id = user_id - self.room_id = room_id - self.our_member_event = our_member_event - self.store = store - - rules = [] - for raw_rule in raw_rules: - rule = dict(raw_rule) - rule['conditions'] = json.loads(raw_rule['conditions']) - rule['actions'] = json.loads(raw_rule['actions']) - rules.append(rule) - - self.rules = list_with_base_rules(rules) - - self.enabled_map = enabled_map - - @staticmethod - def tweaks_for_actions(actions): - tweaks = {} - for a in actions: - if not isinstance(a, dict): - continue - if 'set_tweak' in a and 'value' in a: - tweaks[a['set_tweak']] = a['value'] - return tweaks - - @defer.inlineCallbacks - def actions_for_event(self, ev): - """ - This should take into account notification settings that the user - has configured both globally and per-room when we have the ability - to do such things. - """ - if ev['user_id'] == self.user_id: - # let's assume you probably know about messages you sent yourself - defer.returnValue([]) - - room_id = ev['room_id'] - - # get *our* member event for display name matching - my_display_name = None - - if self.our_member_event: - my_display_name = self.our_member_event[0].content.get("displayname") - - room_members = yield self.store.get_users_in_room(room_id) - room_member_count = len(room_members) - - evaluator = PushRuleEvaluatorForEvent(ev, room_member_count) - - for r in self.rules: - enabled = self.enabled_map.get(r['rule_id'], None) - if enabled is not None and not enabled: - continue - elif enabled is None and not r.get("enabled", True): - # if no override, check enabled on the rule itself - # (may have come from a base rule) - continue - - conditions = r['conditions'] - actions = r['actions'] - - # ignore rules with no actions (we have an explict 'dont_notify') - if len(actions) == 0: - logger.warn( - "Ignoring rule id %s with no actions for user %s", - r['rule_id'], self.user_id - ) - continue - - matches = True - for c in conditions: - matches = evaluator.matches( - c, self.user_id, my_display_name - ) - if not matches: - break - - logger.debug( - "Rule %s %s", - r['rule_id'], "matches" if matches else "doesn't match" - ) - - if matches: - logger.debug( - "%s matches for user %s, event %s", - r['rule_id'], self.user_id, ev['event_id'] - ) - - # filter out dont_notify as we treat an empty actions list - # as dont_notify, and this doesn't take up a row in our database - actions = [x for x in actions if x != 'dont_notify'] - - defer.returnValue(actions) - - logger.debug( - "No rules match for user %s, event %s", - self.user_id, ev['event_id'] - ) - defer.returnValue(PushRuleEvaluator.DEFAULT_ACTIONS) +def tweaks_for_actions(actions): + tweaks = {} + for a in actions: + if not isinstance(a, dict): + continue + if 'set_tweak' in a and 'value' in a: + tweaks[a['set_tweak']] = a['value'] + return tweaks class PushRuleEvaluatorForEvent(object): -- cgit 1.5.1 From e1e042f2a1bc489c922f0deccfac54572788c933 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 17:09:36 +0100 Subject: Add comments on min_stream_id saying that the min stream id won't be completely accurate all the time --- synapse/handlers/receipts.py | 1 + synapse/push/httppusher.py | 2 ++ 2 files changed, 3 insertions(+) (limited to 'synapse/push') diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 26b0368080..a390a1b8bd 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -111,6 +111,7 @@ class ReceiptsHandler(BaseHandler): self.notifier.on_new_event( "receipt_key", max_batch_id, rooms=affected_room_ids ) + # Note that the min here shouldn't be relied upon to be accurate. self.hs.get_pusherpool().on_new_receipts( min_batch_id, max_batch_id, affected_room_ids ) diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index cc030a57a0..0a1d3817de 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -87,6 +87,8 @@ class HttpPusher(object): @defer.inlineCallbacks def on_new_receipts(self, min_stream_id, max_stream_id): + # Note that the min here shouldn't be relied upon to be accurate. + # We could check the receipts are actually m.read receipts here, # but currently that's the only type of receipt anyway... badge = yield push_tools.get_badge_count(self.hs, self.user_id) -- cgit 1.5.1 From fa129ce5b5cfa2b6cbbb0f1a884f47d740ba1300 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 17:12:29 +0100 Subject: Add measure blocks --- synapse/push/httppusher.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 0a1d3817de..ea45b603c6 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -21,6 +21,8 @@ import logging import push_rule_evaluator import push_tools +from synapse.util.metrics import Measure + logger = logging.getLogger(__name__) @@ -82,8 +84,9 @@ class HttpPusher(object): @defer.inlineCallbacks def on_new_notifications(self, min_stream_ordering, max_stream_ordering): - self.max_stream_ordering = max_stream_ordering - yield self._process() + with Measure(self.clock, "push.on_new_notifications"): + self.max_stream_ordering = max_stream_ordering + yield self._process() @defer.inlineCallbacks def on_new_receipts(self, min_stream_id, max_stream_id): @@ -91,12 +94,14 @@ class HttpPusher(object): # We could check the receipts are actually m.read receipts here, # but currently that's the only type of receipt anyway... - badge = yield push_tools.get_badge_count(self.hs, self.user_id) - yield self.send_badge(badge) + with Measure(self.clock, "push.on_new_receipts"): + badge = yield push_tools.get_badge_count(self.hs, self.user_id) + yield self.send_badge(badge) @defer.inlineCallbacks def on_timer(self): - yield self._process() + with Measure(self.clock, "push.on_timer"): + yield self._process() def on_stop(self): if self.timed_call: -- cgit 1.5.1 From 25cd5bb697996c1764c7746e4dfc1d8fffaaf8b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 17:22:14 +0100 Subject: defer.gatherResults rather than doing all the pokes in series --- synapse/push/pusherpool.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 7b1ce81e9a..8da444179c 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -123,10 +123,17 @@ class PusherPool: users_affected = yield self.store.get_push_action_users_in_range( min_stream_id, max_stream_id ) + + deferreds = [] + for u in users_affected: if u in self.pushers: for p in self.pushers[u].values(): - yield p.on_new_notifications(min_stream_id, max_stream_id) + deferreds.append( + p.on_new_notifications(min_stream_id, max_stream_id) + ) + + yield defer.gatherResults(deferreds) except: logger.exception("Exception in pusher on_new_notifications") @@ -141,10 +148,17 @@ class PusherPool: ) # This returns a tuple, user_id is at index 3 users_affected = set([r[3] for r in updated_receipts]) + + deferreds = [] + for u in users_affected: if u in self.pushers: for p in self.pushers[u].values(): - yield p.on_new_receipts(min_stream_id, max_stream_id) + deferreds.append( + p.on_new_receipts(min_stream_id, max_stream_id) + ) + + yield defer.gatherResults(deferreds) except: logger.exception("Exception in pusher on_new_receipts") -- cgit 1.5.1 From 6ec02e9ecf7ffe3d3737a69a480939a07d62428b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 17:24:05 +0100 Subject: indenting --- synapse/push/pusherpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 8da444179c..ba513601e7 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -155,7 +155,7 @@ class PusherPool: if u in self.pushers: for p in self.pushers[u].values(): deferreds.append( - p.on_new_receipts(min_stream_id, max_stream_id) + p.on_new_receipts(min_stream_id, max_stream_id) ) yield defer.gatherResults(deferreds) -- cgit 1.5.1 From 15e0f1696f2556f72b65f14466df51e9a9f00c4b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 17:31:08 +0100 Subject: Wrap process in a flag so we don't process whist already processing. --- synapse/push/httppusher.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index ea45b603c6..a0d0234e2e 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -48,6 +48,7 @@ class HttpPusher(object): self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC self.failing_since = pusherdict['failing_since'] self.timed_call = None + self.processing = False # This is the highest stream ordering we know it's safe to process. # When new events arrive, we'll be given a window of new events: we @@ -109,6 +110,14 @@ class HttpPusher(object): @defer.inlineCallbacks def _process(self): + try: + self.processing = True + yield self._unsafe_process() + finally: + self.processing = False + + @defer.inlineCallbacks + def _unsafe_process(self): unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( self.user_id, self.last_stream_ordering, self.max_stream_ordering ) -- cgit 1.5.1 From a4a31fa8dca0199e8958b71a39b749eb4f6efb9e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Apr 2016 17:37:19 +0100 Subject: Only pass in what we need --- synapse/push/httppusher.py | 8 +++++--- synapse/push/push_tools.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index a0d0234e2e..9f51106d0f 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -96,7 +96,9 @@ class HttpPusher(object): # We could check the receipts are actually m.read receipts here, # but currently that's the only type of receipt anyway... with Measure(self.clock, "push.on_new_receipts"): - badge = yield push_tools.get_badge_count(self.hs, self.user_id) + badge = yield push_tools.get_badge_count( + self.hs.get_datastore(), self.user_id + ) yield self.send_badge(badge) @defer.inlineCallbacks @@ -185,7 +187,7 @@ class HttpPusher(object): defer.returnValue(True) tweaks = push_rule_evaluator.tweaks_for_actions(push_action['actions']) - badge = yield push_tools.get_badge_count(self.hs, self.user_id) + badge = yield push_tools.get_badge_count(self.hs.get_datastore(), self.user_id) event = yield self.store.get_event(push_action['event_id'], allow_none=True) if event is None: @@ -215,7 +217,7 @@ class HttpPusher(object): @defer.inlineCallbacks def _build_notification_dict(self, event, tweaks, badge): - ctx = yield push_tools.get_context_for_event(self.hs, event) + ctx = yield push_tools.get_context_for_event(self.hs.get_datastore(), event) d = { 'notification': { diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py index e1e61e49e8..e71d01e77d 100644 --- a/synapse/push/push_tools.py +++ b/synapse/push/push_tools.py @@ -17,13 +17,13 @@ from twisted.internet import defer @defer.inlineCallbacks -def get_badge_count(hs, user_id): +def get_badge_count(store, user_id): invites, joins = yield defer.gatherResults([ - hs.get_datastore().get_invited_rooms_for_user(user_id), - hs.get_datastore().get_rooms_for_user(user_id), + store.get_invited_rooms_for_user(user_id), + store.get_rooms_for_user(user_id), ], consumeErrors=True) - my_receipts_by_room = yield hs.get_datastore().get_receipts_for_user( + my_receipts_by_room = yield store.get_receipts_for_user( user_id, "m.read", ) @@ -34,7 +34,7 @@ def get_badge_count(hs, user_id): last_unread_event_id = my_receipts_by_room[r.room_id] notifs = yield ( - hs.get_datastore().get_unread_event_push_actions_by_room_for_user( + store.get_unread_event_push_actions_by_room_for_user( r.room_id, user_id, last_unread_event_id ) ) @@ -43,8 +43,8 @@ def get_badge_count(hs, user_id): @defer.inlineCallbacks -def get_context_for_event(hs, ev): - name_aliases = yield hs.get_datastore().get_room_name_and_aliases( +def get_context_for_event(store, ev): + name_aliases = yield store.get_room_name_and_aliases( ev.room_id ) @@ -52,7 +52,7 @@ def get_context_for_event(hs, ev): if name_aliases[0] is not None: ctx['name'] = name_aliases[0] - their_member_events_for_room = yield hs.get_datastore().get_current_state( + their_member_events_for_room = yield store.get_current_state( room_id=ev.room_id, event_type='m.room.member', state_key=ev.user_id -- cgit 1.5.1 From ce3fe52498547144191fadf8ff0a8cb6d244334e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Apr 2016 14:02:38 +0100 Subject: Comment why unsafe process is unsafe --- synapse/push/httppusher.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 9f51106d0f..685c5e48df 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -120,6 +120,11 @@ class HttpPusher(object): @defer.inlineCallbacks def _unsafe_process(self): + """ + Looks for unset notifications and dispatch them, in order + Never call this directly: use _process which will only allow this to + run once per pusher. + """ unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( self.user_id, self.last_stream_ordering, self.max_stream_ordering ) -- cgit 1.5.1 From 52d1008661cebe8551bffd97b938369550851bc6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Apr 2016 14:06:54 +0100 Subject: Unsafe process should call itself if the max has changed --- synapse/push/httppusher.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 685c5e48df..6ef8bf62b3 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -125,6 +125,7 @@ class HttpPusher(object): Never call this directly: use _process which will only allow this to run once per pusher. """ + starting_max_ordering = self.max_stream_ordering unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( self.user_id, self.last_stream_ordering, self.max_stream_ordering ) @@ -185,6 +186,8 @@ class HttpPusher(object): self.timed_call = reactor.callLater(self.backoff_delay, self.on_timer) self.backoff_delay = min(self.backoff_delay * 2, self.MAX_BACKOFF_SEC) break + if self.max_stream_ordering != starting_max_ordering: + self._unsafe_process() @defer.inlineCallbacks def _process_one(self, push_action): -- cgit 1.5.1 From 7b6d51948241090799d3bb9f52a9082b29a27d73 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Apr 2016 14:08:16 +0100 Subject: Make sure max stream ordering only increases --- synapse/push/httppusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 6ef8bf62b3..38b758e6af 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -86,7 +86,7 @@ class HttpPusher(object): @defer.inlineCallbacks def on_new_notifications(self, min_stream_ordering, max_stream_ordering): with Measure(self.clock, "push.on_new_notifications"): - self.max_stream_ordering = max_stream_ordering + self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) yield self._process() @defer.inlineCallbacks -- cgit 1.5.1 From ed3979df5faac6d63990f4230662ff8cdcf59584 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Apr 2016 15:29:59 +0100 Subject: Fix invite pushes * If the event is an invite event, add the invitee to list of user we run push rules for (if they have a pusher etc) * Move invite_for_me to be higher prio than member events otherwise member events matches them * Spell override right --- synapse/push/action_generator.py | 6 +-- synapse/push/baserules.py | 72 ++++++++++++++++---------------- synapse/push/bulk_push_rule_evaluator.py | 12 +++++- synapse/storage/pusher.py | 7 ++++ 4 files changed, 58 insertions(+), 39 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/action_generator.py b/synapse/push/action_generator.py index 84efcdd184..59e512f507 100644 --- a/synapse/push/action_generator.py +++ b/synapse/push/action_generator.py @@ -15,7 +15,7 @@ from twisted.internet import defer -from .bulk_push_rule_evaluator import evaluator_for_room_id +from .bulk_push_rule_evaluator import evaluator_for_event import logging @@ -35,8 +35,8 @@ class ActionGenerator: @defer.inlineCallbacks def handle_push_actions_for_event(self, event, context, handler): - bulk_evaluator = yield evaluator_for_room_id( - event.room_id, self.hs, self.store + bulk_evaluator = yield evaluator_for_event( + event, self.hs, self.store ) actions_by_user = yield bulk_evaluator.action_for_event_by_user( diff --git a/synapse/push/baserules.py b/synapse/push/baserules.py index 6add94beeb..8a174feeaf 100644 --- a/synapse/push/baserules.py +++ b/synapse/push/baserules.py @@ -79,7 +79,7 @@ def make_base_append_rules(kind, modified_base_rules): rules = [] if kind == 'override': - rules = BASE_APPEND_OVRRIDE_RULES + rules = BASE_APPEND_OVERRIDE_RULES elif kind == 'underride': rules = BASE_APPEND_UNDERRIDE_RULES elif kind == 'content': @@ -148,7 +148,7 @@ BASE_PREPEND_OVERRIDE_RULES = [ ] -BASE_APPEND_OVRRIDE_RULES = [ +BASE_APPEND_OVERRIDE_RULES = [ { 'rule_id': 'global/override/.m.rule.suppress_notices', 'conditions': [ @@ -163,6 +163,40 @@ BASE_APPEND_OVRRIDE_RULES = [ 'dont_notify', ] }, + # NB. .m.rule.invite_for_me must be higher prio than .m.rule.member_event + # otherwise invites will be matched by .m.rule.member_event + { + 'rule_id': 'global/underride/.m.rule.invite_for_me', + 'conditions': [ + { + 'kind': 'event_match', + 'key': 'type', + 'pattern': 'm.room.member', + '_id': '_member', + }, + { + 'kind': 'event_match', + 'key': 'content.membership', + 'pattern': 'invite', + '_id': '_invite_member', + }, + { + 'kind': 'event_match', + 'key': 'state_key', + 'pattern_type': 'user_id' + }, + ], + 'actions': [ + 'notify', + { + 'set_tweak': 'sound', + 'value': 'default' + }, { + 'set_tweak': 'highlight', + 'value': False + } + ] + }, # Will we sometimes want to know about people joining and leaving? # Perhaps: if so, this could be expanded upon. Seems the most usual case # is that we don't though. We add this override rule so that even if @@ -251,38 +285,6 @@ BASE_APPEND_UNDERRIDE_RULES = [ } ] }, - { - 'rule_id': 'global/underride/.m.rule.invite_for_me', - 'conditions': [ - { - 'kind': 'event_match', - 'key': 'type', - 'pattern': 'm.room.member', - '_id': '_member', - }, - { - 'kind': 'event_match', - 'key': 'content.membership', - 'pattern': 'invite', - '_id': '_invite_member', - }, - { - 'kind': 'event_match', - 'key': 'state_key', - 'pattern_type': 'user_id' - }, - ], - 'actions': [ - 'notify', - { - 'set_tweak': 'sound', - 'value': 'default' - }, { - 'set_tweak': 'highlight', - 'value': False - } - ] - }, { 'rule_id': 'global/underride/.m.rule.message', 'conditions': [ @@ -315,7 +317,7 @@ for r in BASE_PREPEND_OVERRIDE_RULES: r['default'] = True BASE_RULE_IDS.add(r['rule_id']) -for r in BASE_APPEND_OVRRIDE_RULES: +for r in BASE_APPEND_OVERRIDE_RULES: r['priority_class'] = PRIORITY_CLASS_MAP['override'] r['default'] = True BASE_RULE_IDS.add(r['rule_id']) diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 7f94591dcb..49216f0c15 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -69,7 +69,8 @@ def _get_rules(room_id, user_ids, store): @defer.inlineCallbacks -def evaluator_for_room_id(room_id, hs, store): +def evaluator_for_event(event, hs, store): + room_id = event.room_id users_with_pushers = yield store.get_users_with_pushers_in_room(room_id) receipts = yield store.get_receipts_for_room(room_id, "m.read") @@ -79,6 +80,15 @@ def evaluator_for_room_id(room_id, hs, store): if hs.is_mine_id(r['user_id']): user_ids.add(r['user_id']) + # if this event is an invite event, we may need to run rules for the user + # who's been invited, otherwise they won't get told they've been invited + if event.type == 'm.room.member' and event.content['membership'] == 'invite': + invited_user = event.state_key + if invited_user and hs.is_mine_id(invited_user): + has_pusher = yield store.user_has_pusher(invited_user) + if has_pusher: + user_ids.add(invited_user) + user_ids = list(user_ids) rules_by_user = yield _get_rules(room_id, user_ids, store) diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index b34a30a8fb..19888a8e76 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -49,6 +49,13 @@ class PusherStore(SQLBaseStore): return rows + @defer.inlineCallbacks + def user_has_pusher(self, user_id): + ret = yield self._simple_select_one_onecol( + "pushers", {"user_name": user_id}, "id", allow_none=True + ) + defer.returnValue(ret is not None) + @defer.inlineCallbacks def get_pushers_by_app_id_and_pushkey(self, app_id, pushkey): def r(txn): -- cgit 1.5.1 From d96a070a3a6da4e2ff868f656a28f1bfd5f3ea82 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Apr 2016 16:49:39 +0100 Subject: Actually check if we;re processing --- synapse/push/httppusher.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 38b758e6af..b3b11c5f43 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -112,6 +112,8 @@ class HttpPusher(object): @defer.inlineCallbacks def _process(self): + if self.processing: + return try: self.processing = True yield self._unsafe_process() -- cgit 1.5.1 From 9bb041791ce09209a21c61cadb79c52cba9ac3d9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Apr 2016 12:48:30 +0100 Subject: Run unsafe proces in a loop until we've caught up and wrap unsafe process in a try block --- synapse/push/httppusher.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index b3b11c5f43..57f0a69e03 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -116,7 +116,16 @@ class HttpPusher(object): return try: self.processing = True - yield self._unsafe_process() + # if the max ordering changes while we're running _unsafe_process, + # call it again, and so on until we've caught up. + while True: + starting_max_ordering = self.max_stream_ordering + try: + yield self._unsafe_process() + except: + logger.exception("Exception processing notifs") + if self.max_stream_ordering == starting_max_ordering: + break finally: self.processing = False @@ -127,7 +136,7 @@ class HttpPusher(object): Never call this directly: use _process which will only allow this to run once per pusher. """ - starting_max_ordering = self.max_stream_ordering + unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( self.user_id, self.last_stream_ordering, self.max_stream_ordering ) @@ -188,8 +197,6 @@ class HttpPusher(object): self.timed_call = reactor.callLater(self.backoff_delay, self.on_timer) self.backoff_delay = min(self.backoff_delay * 2, self.MAX_BACKOFF_SEC) break - if self.max_stream_ordering != starting_max_ordering: - self._unsafe_process() @defer.inlineCallbacks def _process_one(self, push_action): -- cgit 1.5.1 From 790f5848b2b206fb4bc76c2b282ffe77b9b24f43 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 11 Apr 2016 16:10:39 +0100 Subject: Fix the rule_id for .m.rule.invite_for_me (#715) --- synapse/push/baserules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/baserules.py b/synapse/push/baserules.py index 8a174feeaf..024c14904f 100644 --- a/synapse/push/baserules.py +++ b/synapse/push/baserules.py @@ -166,7 +166,7 @@ BASE_APPEND_OVERRIDE_RULES = [ # NB. .m.rule.invite_for_me must be higher prio than .m.rule.member_event # otherwise invites will be matched by .m.rule.member_event { - 'rule_id': 'global/underride/.m.rule.invite_for_me', + 'rule_id': 'global/override/.m.rule.invite_for_me', 'conditions': [ { 'kind': 'event_match', -- cgit 1.5.1 From 65141161f6ead75c4f07c548447704a302686ebf Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Apr 2016 16:25:26 +0100 Subject: Unused member variable --- synapse/push/pusherpool.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index ba513601e7..aa095f9d9b 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -32,7 +32,6 @@ class PusherPool: self.store = self.hs.get_datastore() self.clock = self.hs.get_clock() self.pushers = {} - self.last_pusher_started = -1 @defer.inlineCallbacks def start(self): -- cgit 1.5.1 From 7be1065b8f6b6a4e865342f7fdadd00474b624ca Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 14 Apr 2016 11:26:15 +0100 Subject: Add extra Measure --- synapse/push/httppusher.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 57f0a69e03..0ceff661e7 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -114,20 +114,22 @@ class HttpPusher(object): def _process(self): if self.processing: return - try: - self.processing = True - # if the max ordering changes while we're running _unsafe_process, - # call it again, and so on until we've caught up. - while True: - starting_max_ordering = self.max_stream_ordering - try: - yield self._unsafe_process() - except: - logger.exception("Exception processing notifs") - if self.max_stream_ordering == starting_max_ordering: - break - finally: - self.processing = False + + with Measure(self.clock, "push._process"): + try: + self.processing = True + # if the max ordering changes while we're running _unsafe_process, + # call it again, and so on until we've caught up. + while True: + starting_max_ordering = self.max_stream_ordering + try: + yield self._unsafe_process() + except: + logger.exception("Exception processing notifs") + if self.max_stream_ordering == starting_max_ordering: + break + finally: + self.processing = False @defer.inlineCallbacks def _unsafe_process(self): -- cgit 1.5.1 From 56da835eafa23c911749174d1d9a4e89d9b34643 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 14 Apr 2016 11:33:50 +0100 Subject: Add necessary logging contexts --- synapse/push/httppusher.py | 49 +++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 24 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 0ceff661e7..e80b7dae51 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -21,6 +21,7 @@ import logging import push_rule_evaluator import push_tools +from synapse.util.logcontext import LoggingContext from synapse.util.metrics import Measure logger = logging.getLogger(__name__) @@ -85,9 +86,8 @@ class HttpPusher(object): @defer.inlineCallbacks def on_new_notifications(self, min_stream_ordering, max_stream_ordering): - with Measure(self.clock, "push.on_new_notifications"): - self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) - yield self._process() + self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) + yield self._process() @defer.inlineCallbacks def on_new_receipts(self, min_stream_id, max_stream_id): @@ -95,16 +95,16 @@ class HttpPusher(object): # We could check the receipts are actually m.read receipts here, # but currently that's the only type of receipt anyway... - with Measure(self.clock, "push.on_new_receipts"): - badge = yield push_tools.get_badge_count( - self.hs.get_datastore(), self.user_id - ) + with LoggingContext("push._process"): + with Measure(self.clock, "push.on_new_receipts"): + badge = yield push_tools.get_badge_count( + self.hs.get_datastore(), self.user_id + ) yield self.send_badge(badge) @defer.inlineCallbacks def on_timer(self): - with Measure(self.clock, "push.on_timer"): - yield self._process() + yield self._process() def on_stop(self): if self.timed_call: @@ -115,21 +115,22 @@ class HttpPusher(object): if self.processing: return - with Measure(self.clock, "push._process"): - try: - self.processing = True - # if the max ordering changes while we're running _unsafe_process, - # call it again, and so on until we've caught up. - while True: - starting_max_ordering = self.max_stream_ordering - try: - yield self._unsafe_process() - except: - logger.exception("Exception processing notifs") - if self.max_stream_ordering == starting_max_ordering: - break - finally: - self.processing = False + with LoggingContext("push._process"): + with Measure(self.clock, "push._process"): + try: + self.processing = True + # if the max ordering changes while we're running _unsafe_process, + # call it again, and so on until we've caught up. + while True: + starting_max_ordering = self.max_stream_ordering + try: + yield self._unsafe_process() + except: + logger.exception("Exception processing notifs") + if self.max_stream_ordering == starting_max_ordering: + break + finally: + self.processing = False @defer.inlineCallbacks def _unsafe_process(self): -- cgit 1.5.1 From 2ae91a9e2f5632bcaa32154d457ddd39a96e981e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 14 Apr 2016 11:37:50 +0100 Subject: Make send_badge private --- synapse/push/httppusher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index e80b7dae51..b939d889fb 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -100,7 +100,7 @@ class HttpPusher(object): badge = yield push_tools.get_badge_count( self.hs.get_datastore(), self.user_id ) - yield self.send_badge(badge) + yield self._send_badge(badge) @defer.inlineCallbacks def on_timer(self): @@ -294,7 +294,7 @@ class HttpPusher(object): defer.returnValue(rejected) @defer.inlineCallbacks - def send_badge(self, badge): + def _send_badge(self, badge): logger.info("Sending updated badge count %d to %r", badge, self.user_id) d = { 'notification': { -- cgit 1.5.1 From e40f25ebe1715324f52fb30741e0b58e3038f4cc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 14 Apr 2016 11:54:14 +0100 Subject: Rename log context --- synapse/push/httppusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index b939d889fb..6268ea7b00 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -95,7 +95,7 @@ class HttpPusher(object): # We could check the receipts are actually m.read receipts here, # but currently that's the only type of receipt anyway... - with LoggingContext("push._process"): + with LoggingContext("push.on_new_receipts"): with Measure(self.clock, "push.on_new_receipts"): badge = yield push_tools.get_badge_count( self.hs.get_datastore(), self.user_id -- cgit 1.5.1 From 86e9bbc74e69d0f8c772c01c363a38594bc6bdf2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 14 Apr 2016 11:56:52 +0100 Subject: Add missing yield --- synapse/push/httppusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 6268ea7b00..6950a20632 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -149,7 +149,7 @@ class HttpPusher(object): if processed: self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC self.last_stream_ordering = push_action['stream_ordering'] - self.store.update_pusher_last_stream_ordering_and_success( + yield self.store.update_pusher_last_stream_ordering_and_success( self.app_id, self.pushkey, self.user_id, self.last_stream_ordering, self.clock.time_msec() -- cgit 1.5.1 From 757e2c79b42d0c867fd21b262b87bc9da4a1bdd6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Apr 2016 12:02:50 +0100 Subject: Don't push for everyone who ever sent an RR to the room --- synapse/push/bulk_push_rule_evaluator.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 49216f0c15..f97df36d80 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -71,13 +71,25 @@ def _get_rules(room_id, user_ids, store): @defer.inlineCallbacks def evaluator_for_event(event, hs, store): room_id = event.room_id + + # users in the room who have pushers need to get push rules run because + # that's how their pushers work users_with_pushers = yield store.get_users_with_pushers_in_room(room_id) + + # We also will want to generate notifs for other people in the room so + # their unread countss are correct in the event stream, but to avoid + # generating them for bot / AS users etc, we only do so for people who've + # sent a read receipt into the room. + + all_in_room = yield store.get_users_in_room(room_id) + all_in_room = set(all_in_room) + receipts = yield store.get_receipts_for_room(room_id, "m.read") # any users with pushers must be ours: they have pushers user_ids = set(users_with_pushers) for r in receipts: - if hs.is_mine_id(r['user_id']): + if hs.is_mine_id(r['user_id']) and r['user_id'] in all_in_room: user_ids.add(r['user_id']) # if this event is an invite event, we may need to run rules for the user -- cgit 1.5.1 From 9ae64c9910e8d6cef2dcb9aba6330e497195b8c7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 14 Apr 2016 13:42:22 +0100 Subject: Measure push action generator --- synapse/push/action_generator.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/action_generator.py b/synapse/push/action_generator.py index 59e512f507..a0160994b7 100644 --- a/synapse/push/action_generator.py +++ b/synapse/push/action_generator.py @@ -17,6 +17,8 @@ from twisted.internet import defer from .bulk_push_rule_evaluator import evaluator_for_event +from synapse.util.metrics import Measure + import logging logger = logging.getLogger(__name__) @@ -25,6 +27,7 @@ logger = logging.getLogger(__name__) class ActionGenerator: def __init__(self, hs): self.hs = hs + self.clock = hs.get_clock() self.store = hs.get_datastore() # really we want to get all user ids and all profile tags too, # since we want the actions for each profile tag for every user and @@ -35,14 +38,15 @@ class ActionGenerator: @defer.inlineCallbacks def handle_push_actions_for_event(self, event, context, handler): - bulk_evaluator = yield evaluator_for_event( - event, self.hs, self.store - ) - - actions_by_user = yield bulk_evaluator.action_for_event_by_user( - event, handler, context.current_state - ) - - context.push_actions = [ - (uid, actions) for uid, actions in actions_by_user.items() - ] + with Measure(self.clock, "handle_push_actions_for_event"): + bulk_evaluator = yield evaluator_for_event( + event, self.hs, self.store + ) + + actions_by_user = yield bulk_evaluator.action_for_event_by_user( + event, handler, context.current_state + ) + + context.push_actions = [ + (uid, actions) for uid, actions in actions_by_user.items() + ] -- cgit 1.5.1 From 07d765209dea12229e70a09784e647611acabcda Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Apr 2016 14:24:36 +0100 Subject: First bits of emailpusher Mostly logic of when to send an email --- synapse/push/emailpusher.py | 214 +++++++++++++++++++++ synapse/push/pusher.py | 4 +- synapse/storage/event_push_actions.py | 57 +++++- synapse/storage/events.py | 2 + synapse/storage/pusher.py | 27 +++ synapse/storage/schema/delta/31/events.sql | 16 ++ .../storage/schema/delta/31/pusher_throttle.sql | 23 +++ 7 files changed, 335 insertions(+), 8 deletions(-) create mode 100644 synapse/push/emailpusher.py create mode 100644 synapse/storage/schema/delta/31/events.sql create mode 100644 synapse/storage/schema/delta/31/pusher_throttle.sql (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py new file mode 100644 index 0000000000..f9954df392 --- /dev/null +++ b/synapse/push/emailpusher.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer, reactor + +import logging + +from synapse.util.metrics import Measure +from synapse.util.async import run_on_reactor + +logger = logging.getLogger(__name__) + +# The amount of time we always wait before ever emailing about a notification +# (to give the user a chance to respond to other push or notice the window) +DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 + +THROTTLE_START_MS = 2 * 60 * 1000 +THROTTLE_MAX_MS = (2 * 60 * 1000) * (2**11) # ~3 days + +# If no event triggers a notification for this long after the previous, +# the throttle is released. +THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2**11) # ~3 days + + +class EmailPusher(object): + """ + A pusher that sends email notifications about events (approximately) + when they happen. + This shares quite a bit of code with httpusher: it would be good to + factor out the common parts + """ + def __init__(self, hs, pusherdict): + self.hs = hs + self.store = self.hs.get_datastore() + self.clock = self.hs.get_clock() + self.pusher_id = pusherdict['id'] + self.user_id = pusherdict['user_name'] + self.app_id = pusherdict['app_id'] + self.email = pusherdict['pushkey'] + self.last_stream_ordering = pusherdict['last_stream_ordering'] + self.timed_call = None + self.throttle_params = None + + # See httppusher + self.max_stream_ordering = None + + @defer.inlineCallbacks + def on_started(self): + self.throttle_params = yield self.store.get_throttle_params_by_room( + self.pusher_id + ) + yield self._process() + + @defer.inlineCallbacks + def on_new_notifications(self, min_stream_ordering, max_stream_ordering): + with Measure(self.clock, "push.on_new_notifications"): + self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) + yield self._process() + + @defer.inlineCallbacks + def on_timer(self): + self.timed_call = None + with Measure(self.clock, "push.on_timer"): + yield self._process() + + @defer.inlineCallbacks + def _process(self): + last_notifs = yield self.store.get_time_of_latest_push_action_by_room_for_user( + self.user_id + ) + + unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( + self.user_id, self.last_stream_ordering, self.max_stream_ordering + ) + + soonest_due_at = None + + for push_action in unprocessed: + received_at = push_action['received_ts'] + if received_at is None: + received_at = 0 + notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS + + room_ready_at = self.room_ready_to_notify_at( + push_action['room_id'], self.get_room_last_notif_ts( + last_notifs, push_action['room_id'] + ) + ) + + should_notify_at = max(notif_ready_at, room_ready_at) + + if should_notify_at < self.clock.time_msec(): + # one of our notifications is ready for sending, so we send + # *one* email updating the user on their notifications, + # we then consider all previously outstanding notifications + # to be delivered. + yield self.send_notification(push_action) + + yield self.save_last_stream_ordering_and_success(max([ + ea['stream_ordering'] for ea in unprocessed + ])) + yield self.sent_notif_update_throttle( + push_action['room_id'], push_action + ) + else: + if soonest_due_at is None or should_notify_at < soonest_due_at: + soonest_due_at = should_notify_at + + if self.timed_call is not None: + self.timed_call.cancel() + self.timed_call = None + + if soonest_due_at is not None: + self.timed_call = reactor.callLater( + self.seconds_until(soonest_due_at), self.on_timer + ) + + @defer.inlineCallbacks + def save_last_stream_ordering_and_success(self, last_stream_ordering): + self.last_stream_ordering = last_stream_ordering + yield self.store.update_pusher_last_stream_ordering_and_success( + self.app_id, self.email, self.user_id, + last_stream_ordering, self.clock.time_msec() + ) + + def seconds_until(self, ts_msec): + return (ts_msec - self.clock.time_msec()) / 1000 + + def get_room_last_notif_ts(self, last_notif_by_room, room_id): + if room_id in last_notif_by_room: + return last_notif_by_room[room_id] + else: + return 0 + + def get_room_throttle_ms(self, room_id): + if room_id in self.throttle_params: + return self.throttle_params[room_id]["throttle_ms"] + else: + return 0 + + def get_room_last_sent_ts(self, room_id): + if room_id in self.throttle_params: + return self.throttle_params[room_id]["last_sent_ts"] + else: + return 0 + + def room_ready_to_notify_at(self, room_id, last_notif_time): + """ + Determines whether throttling should prevent us from sending an email + for the given room + Returns: True if we should send, False if we should not + """ + last_sent_ts = self.get_room_last_sent_ts(room_id) + throttle_ms = self.get_room_throttle_ms(room_id) + + may_send_at = last_sent_ts + throttle_ms + return may_send_at + + @defer.inlineCallbacks + def sent_notif_update_throttle(self, room_id, notified_push_action): + # We have sent a notification, so update the throttle accordingly. + # If the event that triggered the notif happened more than + # THROTTLE_RESET_AFTER_MS after the previous one that triggered a + # notif, we release the throttle. Otherwise, the throttle is increased. + time_of_previous_notifs = yield self.store.get_time_of_last_push_action_before( + notified_push_action['stream_ordering'] + ) + + time_of_this_notifs = notified_push_action['received_ts'] + + if time_of_previous_notifs is not None and time_of_this_notifs is not None: + gap = time_of_this_notifs - time_of_previous_notifs + else: + # if we don't know the arrival time of one of the notifs (it was not + # stored prior to email notification code) then assume a gap of + # zero which will just not reset the throttle + gap = 0 + + current_throttle_ms = self.get_room_throttle_ms(room_id) + + if gap > THROTTLE_RESET_AFTER_MS: + new_throttle_ms = THROTTLE_START_MS + else: + if current_throttle_ms == 0: + new_throttle_ms = THROTTLE_START_MS + else: + new_throttle_ms = min( + current_throttle_ms * 2, + THROTTLE_MAX_MS + ) + self.throttle_params[room_id] = { + "last_sent_ts": self.clock.time_msec(), + "throttle_ms": new_throttle_ms + } + yield self.store.set_throttle_params( + self.pusher_id, room_id, self.throttle_params[room_id] + ) + + @defer.inlineCallbacks + def send_notification(self, push_action): + yield run_on_reactor() + logger.error("sending notif email for user %r", self.user_id) \ No newline at end of file diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index 4960837504..f7c3021fcc 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,7 +1,9 @@ from httppusher import HttpPusher +from emailpusher import EmailPusher PUSHER_TYPES = { - 'http': HttpPusher + 'http': HttpPusher, + 'email': EmailPusher, } diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index 86a98b6f11..ad512b2f07 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -118,15 +118,17 @@ class EventPushActionsStore(SQLBaseStore): max_stream_ordering=None): def get_after_receipt(txn): sql = ( - "SELECT ep.event_id, ep.stream_ordering, ep.actions " + "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions, " + "e.received_ts " "FROM event_push_actions AS ep, (" - " SELECT room_id, user_id," - " max(topological_ordering) as topological_ordering," - " max(stream_ordering) as stream_ordering" + " SELECT room_id, user_id, " + " max(topological_ordering) as topological_ordering, " + " max(stream_ordering) as stream_ordering " " FROM events" " NATURAL JOIN receipts_linearized WHERE receipt_type = 'm.read'" " GROUP BY room_id, user_id" ") AS rl " + "NATURAL JOIN events e " "WHERE" " ep.room_id = rl.room_id" " AND (" @@ -153,8 +155,10 @@ class EventPushActionsStore(SQLBaseStore): def get_no_receipt(txn): sql = ( - "SELECT ep.event_id, ep.stream_ordering, ep.actions " + "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions, " + "e.received_ts " "FROM event_push_actions AS ep " + "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " "WHERE ep.room_id not in (" " SELECT room_id FROM events NATURAL JOIN receipts_linearized" " WHERE receipt_type = 'm.read' AND user_id = ? " @@ -175,11 +179,30 @@ class EventPushActionsStore(SQLBaseStore): defer.returnValue([ { "event_id": row[0], - "stream_ordering": row[1], - "actions": json.loads(row[2]), + "room_id": row[1], + "stream_ordering": row[2], + "actions": json.loads(row[3]), + "received_ts": row[4], } for row in after_read_receipt + no_read_receipt ]) + @defer.inlineCallbacks + def get_time_of_last_push_action_before(self, stream_ordering): + def f(txn): + sql = ( + "SELECT e.received_ts " + "FROM event_push_actions AS ep " + "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " + "WHERE ep.stream_ordering > ? " + "ORDER BY ep.stream_ordering ASC " + "LIMIT 1" + ) + txn.execute(sql, (stream_ordering,)) + return txn.fetchone() + result = yield self.runInteraction("get_time_of_last_push_action_before", f) + defer.returnValue(result[0] if result is not None else None) + + @defer.inlineCallbacks def get_latest_push_action_stream_ordering(self): def f(txn): @@ -190,6 +213,26 @@ class EventPushActionsStore(SQLBaseStore): ) defer.returnValue(result[0] or 0) + @defer.inlineCallbacks + def get_time_of_latest_push_action_by_room_for_user(self, user_id): + """ + Returns only the received_ts of the last notification in each of the + user's rooms, in a dict by room_id + """ + def f(txn): + txn.execute( + "SELECT ep.room_id, MAX(e.received_ts) " + "FROM event_push_actions AS ep " + "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " + "GROUP BY ep.room_id" + ) + return txn.fetchall() + result = yield self.runInteraction( + "get_time_of_latest_push_action_by_room_for_user", f + ) + + defer.returnValue({row[0]: row[1] for row in result}) + def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): # Sad that we have to blow away the cache for the whole room here txn.call_after( diff --git a/synapse/storage/events.py b/synapse/storage/events.py index 21487724ed..dd58e001dc 100644 --- a/synapse/storage/events.py +++ b/synapse/storage/events.py @@ -55,6 +55,7 @@ class EventsStore(SQLBaseStore): def __init__(self, hs): super(EventsStore, self).__init__(hs) + self._clock = hs.get_clock() self.register_background_update_handler( self.EVENT_ORIGIN_SERVER_TS_NAME, self._background_reindex_origin_server_ts ) @@ -427,6 +428,7 @@ class EventsStore(SQLBaseStore): "outlier": event.internal_metadata.is_outlier(), "content": encode_json(event.content).decode("UTF-8"), "origin_server_ts": int(event.origin_server_ts), + "received_ts": self._clock.time_msec(), } for event, _ in events_and_contexts ], diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index e5755c0aea..caef9b59a5 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -230,3 +230,30 @@ class PusherStore(SQLBaseStore): {'failing_since': failing_since}, desc="update_pusher_failing_since", ) + + @defer.inlineCallbacks + def get_throttle_params_by_room(self, pusher_id): + res = yield self._simple_select_list( + "pusher_throttle", + {"pusher": pusher_id}, + ["room_id", "last_sent_ts", "throttle_ms"], + desc="get_throttle_params_by_room" + ) + + params_by_room = {} + for row in res: + params_by_room[row["room_id"]] = { + "last_sent_ts": row["last_sent_ts"], + "throttle_ms": row["throttle_ms"] + } + + defer.returnValue(params_by_room) + + @defer.inlineCallbacks + def set_throttle_params(self, pusher_id, room_id, params): + yield self._simple_upsert( + "pusher_throttle", + {"pusher": pusher_id, "room_id": room_id}, + params, + desc="set_throttle_params" + ) \ No newline at end of file diff --git a/synapse/storage/schema/delta/31/events.sql b/synapse/storage/schema/delta/31/events.sql new file mode 100644 index 0000000000..1dd0f9e170 --- /dev/null +++ b/synapse/storage/schema/delta/31/events.sql @@ -0,0 +1,16 @@ +/* Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +ALTER TABLE events ADD COLUMN received_ts BIGINT; diff --git a/synapse/storage/schema/delta/31/pusher_throttle.sql b/synapse/storage/schema/delta/31/pusher_throttle.sql new file mode 100644 index 0000000000..d86d30c13c --- /dev/null +++ b/synapse/storage/schema/delta/31/pusher_throttle.sql @@ -0,0 +1,23 @@ +/* Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +CREATE TABLE pusher_throttle( + pusher BIGINT NOT NULL, + room_id TEXT NOT NULL, + last_sent_ts BIGINT, + throttle_ms BIGINT, + PRIMARY KEY (pusher, room_id) +); -- cgit 1.5.1 From e2a01455af8dbab26b4a005d847f468a51fea6c3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Apr 2016 14:52:58 +0100 Subject: Add single instance & logging stuff Copy the stuff over from http pusher that prevents multiple instances of process running at once and sets up logging and measure blocks. --- synapse/push/emailpusher.py | 47 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index f9954df392..74e3a70562 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -19,6 +19,7 @@ import logging from synapse.util.metrics import Measure from synapse.util.async import run_on_reactor +from synapse.util.logcontext import LoggingContext logger = logging.getLogger(__name__) @@ -56,6 +57,8 @@ class EmailPusher(object): # See httppusher self.max_stream_ordering = None + self.processing = False + @defer.inlineCallbacks def on_started(self): self.throttle_params = yield self.store.get_throttle_params_by_room( @@ -63,20 +66,48 @@ class EmailPusher(object): ) yield self._process() + def on_stop(self): + if self.timed_call: + self.timed_call.cancel() + @defer.inlineCallbacks def on_new_notifications(self, min_stream_ordering, max_stream_ordering): - with Measure(self.clock, "push.on_new_notifications"): - self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) - yield self._process() + self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) + yield self._process() @defer.inlineCallbacks def on_timer(self): self.timed_call = None - with Measure(self.clock, "push.on_timer"): - yield self._process() + yield self._process() @defer.inlineCallbacks def _process(self): + if self.processing: + return + + with LoggingContext("emailpush._process"): + with Measure(self.clock, "emailpush._process"): + try: + self.processing = True + # if the max ordering changes while we're running _unsafe_process, + # call it again, and so on until we've caught up. + while True: + starting_max_ordering = self.max_stream_ordering + try: + yield self._unsafe_process() + except: + logger.exception("Exception processing notifs") + if self.max_stream_ordering == starting_max_ordering: + break + finally: + self.processing = False + + def _unsafe_process(self): + """ + Main logic of the push loop without the wrapper function that sets + up logging, measures and guards against multiple instances of it + being run. + """ last_notifs = yield self.store.get_time_of_latest_push_action_by_room_for_user( self.user_id ) @@ -118,9 +149,9 @@ class EmailPusher(object): if soonest_due_at is None or should_notify_at < soonest_due_at: soonest_due_at = should_notify_at - if self.timed_call is not None: - self.timed_call.cancel() - self.timed_call = None + if self.timed_call is not None: + self.timed_call.cancel() + self.timed_call = None if soonest_due_at is not None: self.timed_call = reactor.callLater( -- cgit 1.5.1 From f63bd4ff4704c9f7b6e23c76720dbd955a60c058 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Apr 2016 13:02:01 +0100 Subject: Send a rather basic email notif Also pep8 fixes --- synapse/config/emailconfig.py | 62 +++++++++++++++++++++++++++++++++++ synapse/config/homeserver.py | 3 +- synapse/push/emailpusher.py | 32 +++++++++++++----- synapse/push/mailer.py | 48 +++++++++++++++++++++++++++ synapse/storage/event_push_actions.py | 1 - synapse/storage/pusher.py | 2 +- 6 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 synapse/config/emailconfig.py create mode 100644 synapse/push/mailer.py (limited to 'synapse/push') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py new file mode 100644 index 0000000000..978826627b --- /dev/null +++ b/synapse/config/emailconfig.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file can't be called email.py because if it is, we cannot: +import email.utils + +from ._base import Config + + +class EmailConfig(Config): + """ + Email Configuration + """ + + def read_config(self, config): + email_config = config.get("email", None) + if email_config: + self.email_enable_notifs = email_config.get("enable_notifs", True) + if ( + "smtp_host" not in email_config or + "smtp_port" not in email_config or + "notif_from" not in email_config + ): + raise RuntimeError( + "You must set smtp_host, smtp_port and notif_from " + "to send email notifications" + ) + + self.email_smtp_host = email_config["smtp_host"] + self.email_smtp_port = email_config["smtp_port"] + self.email_notif_from = email_config["notif_from"] + + # make sure it's valid + parsed = email.utils.parseaddr(self.email_notif_from) + if parsed[1] == '': + raise RuntimeError("Invalid notif_from address") + else: + self.email_enable_notifs = False + self.email_smtp_host = None + self.email_smtp_port = None + self.email_notif_from = None + + def default_config(self, config_dir_path, server_name, **kwargs): + return """ + # Enable sending emails for notification events + #email_config: + # enable_notifs: false + # smtp_host: "localhost" + # smtp_port: 25 + """ diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 9a80ac39ec..fc2445484c 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -31,13 +31,14 @@ from .cas import CasConfig from .password import PasswordConfig from .jwt import JWTConfig from .ldap import LDAPConfig +from .emailconfig import EmailConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, RatelimitConfig, ContentRepositoryConfig, CaptchaConfig, VoipConfig, RegistrationConfig, MetricsConfig, ApiConfig, AppServiceConfig, KeyConfig, SAML2Config, CasConfig, - JWTConfig, LDAPConfig, PasswordConfig,): + JWTConfig, LDAPConfig, PasswordConfig, EmailConfig,): pass diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 74e3a70562..820c8f8467 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -18,9 +18,10 @@ from twisted.internet import defer, reactor import logging from synapse.util.metrics import Measure -from synapse.util.async import run_on_reactor from synapse.util.logcontext import LoggingContext +from mailer import Mailer + logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification @@ -28,11 +29,11 @@ logger = logging.getLogger(__name__) DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 -THROTTLE_MAX_MS = (2 * 60 * 1000) * (2**11) # ~3 days +THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days # If no event triggers a notification for this long after the previous, # the throttle is released. -THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2**11) # ~3 days +THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days class EmailPusher(object): @@ -59,12 +60,22 @@ class EmailPusher(object): self.processing = False + if self.hs.config.email_enable_notifs: + self.mailer = Mailer( + self.store, + self.hs.config.email_smtp_host, self.hs.config.email_smtp_port, + self.hs.config.email_notif_from, + ) + else: + self.mailer = None + @defer.inlineCallbacks def on_started(self): - self.throttle_params = yield self.store.get_throttle_params_by_room( - self.pusher_id - ) - yield self._process() + if self.mailer is not None: + self.throttle_params = yield self.store.get_throttle_params_by_room( + self.pusher_id + ) + yield self._process() def on_stop(self): if self.timed_call: @@ -102,6 +113,7 @@ class EmailPusher(object): finally: self.processing = False + @defer.inlineCallbacks def _unsafe_process(self): """ Main logic of the push loop without the wrapper function that sets @@ -241,5 +253,7 @@ class EmailPusher(object): @defer.inlineCallbacks def send_notification(self, push_action): - yield run_on_reactor() - logger.error("sending notif email for user %r", self.user_id) \ No newline at end of file + logger.info("Sending notif email for user %r", self.user_id) + yield self.mailer.send_notification_mail( + self.user_id, self.email, push_action + ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py new file mode 100644 index 0000000000..93d3866ec7 --- /dev/null +++ b/synapse/push/mailer.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +import smtplib +import email.utils +import email.mime.multipart +from email.mime.text import MIMEText + + +class Mailer(object): + def __init__(self, store, smtp_host, smtp_port, notif_from): + self.store = store + self.smtp_host = smtp_host + self.smtp_port = smtp_port + self.notif_from = notif_from + + @defer.inlineCallbacks + def send_notification_mail(self, user_id, email_address, push_action): + raw_from = email.utils.parseaddr(self.notif_from)[1] + raw_to = email.utils.parseaddr(email_address)[1] + + if raw_to == '': + raise RuntimeError("Invalid 'to' address") + + plainText = "yo dawg, you got notifications!" + + text_part = MIMEText(plainText, "plain") + text_part['Subject'] = "New Matrix Notifications" + text_part['From'] = self.notif_from + text_part['To'] = email_address + + smtp = smtplib.SMTP(self.smtp_host, self.smtp_port) + smtp.sendmail(raw_from, raw_to, text_part.as_string()) + smtp.quit() \ No newline at end of file diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index ad512b2f07..f2af8bdb36 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -202,7 +202,6 @@ class EventPushActionsStore(SQLBaseStore): result = yield self.runInteraction("get_time_of_last_push_action_before", f) defer.returnValue(result[0] if result is not None else None) - @defer.inlineCallbacks def get_latest_push_action_stream_ordering(self): def f(txn): diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index caef9b59a5..5fb47d418a 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -256,4 +256,4 @@ class PusherStore(SQLBaseStore): {"pusher": pusher_id, "room_id": room_id}, params, desc="set_throttle_params" - ) \ No newline at end of file + ) -- cgit 1.5.1 From 05adc6c2de7def8058d97e9644dddca639886322 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Apr 2016 13:02:45 +0100 Subject: more pep8 --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 93d3866ec7..97cba2ec2b 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -45,4 +45,4 @@ class Mailer(object): smtp = smtplib.SMTP(self.smtp_host, self.smtp_port) smtp.sendmail(raw_from, raw_to, text_part.as_string()) - smtp.quit() \ No newline at end of file + smtp.quit() -- cgit 1.5.1 From 2ed0adb075b745e6586ca88ce7cf6b169460a7d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Apr 2016 18:35:29 +0100 Subject: Generate mails from a template --- synapse/config/emailconfig.py | 35 +++++++++++++++++++++++++---------- synapse/push/emailpusher.py | 12 ++++-------- synapse/push/mailer.py | 30 +++++++++++++++++------------- synapse/python_dependencies.py | 3 +++ 4 files changed, 49 insertions(+), 31 deletions(-) (limited to 'synapse/push') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 978826627b..68fb4d8060 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -28,19 +28,32 @@ class EmailConfig(Config): email_config = config.get("email", None) if email_config: self.email_enable_notifs = email_config.get("enable_notifs", True) - if ( - "smtp_host" not in email_config or - "smtp_port" not in email_config or - "notif_from" not in email_config - ): + + required = [ + "smtp_host", + "smtp_port", + "notif_from", + "template_dir", + "notif_template_html", + + ] + + missing = [] + for k in required: + if k not in email_config: + missing.append(k) + + if (len(missing) > 0): raise RuntimeError( - "You must set smtp_host, smtp_port and notif_from " - "to send email notifications" + "email.enable_notifs is True but required keys are missing: %s" % + (", ".join(["email."+k for k in missing]),) ) self.email_smtp_host = email_config["smtp_host"] self.email_smtp_port = email_config["smtp_port"] self.email_notif_from = email_config["notif_from"] + self.email_template_dir = email_config["template_dir"] + self.email_notif_template_html = email_config["notif_template_html"] # make sure it's valid parsed = email.utils.parseaddr(self.email_notif_from) @@ -48,9 +61,8 @@ class EmailConfig(Config): raise RuntimeError("Invalid notif_from address") else: self.email_enable_notifs = False - self.email_smtp_host = None - self.email_smtp_port = None - self.email_notif_from = None + # Not much point setting defaults for the rest: it would be an + # error for them to be used. def default_config(self, config_dir_path, server_name, **kwargs): return """ @@ -59,4 +71,7 @@ class EmailConfig(Config): # enable_notifs: false # smtp_host: "localhost" # smtp_port: 25 + # notif_from: Your Friendly Matrix Home Server + # template_dir: res/templates + # notif_template_html: notif.html """ diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 820c8f8467..4e21221fb7 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -61,11 +61,7 @@ class EmailPusher(object): self.processing = False if self.hs.config.email_enable_notifs: - self.mailer = Mailer( - self.store, - self.hs.config.email_smtp_host, self.hs.config.email_smtp_port, - self.hs.config.email_notif_from, - ) + self.mailer = Mailer(self.hs) else: self.mailer = None @@ -149,7 +145,7 @@ class EmailPusher(object): # *one* email updating the user on their notifications, # we then consider all previously outstanding notifications # to be delivered. - yield self.send_notification(push_action) + yield self.send_notification(unprocessed) yield self.save_last_stream_ordering_and_success(max([ ea['stream_ordering'] for ea in unprocessed @@ -252,8 +248,8 @@ class EmailPusher(object): ) @defer.inlineCallbacks - def send_notification(self, push_action): + def send_notification(self, push_actions): logger.info("Sending notif email for user %r", self.user_id) yield self.mailer.send_notification_mail( - self.user_id, self.email, push_action + self.user_id, self.email, push_actions ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 97cba2ec2b..0f20d43f75 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -15,34 +15,38 @@ from twisted.internet import defer -import smtplib +from twisted.mail.smtp import sendmail import email.utils import email.mime.multipart from email.mime.text import MIMEText +import jinja2 + class Mailer(object): - def __init__(self, store, smtp_host, smtp_port, notif_from): - self.store = store - self.smtp_host = smtp_host - self.smtp_port = smtp_port - self.notif_from = notif_from + def __init__(self, hs): + self.hs = hs + loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) + env = jinja2.Environment(loader=loader) + self.notif_template = env.get_template(self.hs.config.email_notif_template_html) @defer.inlineCallbacks - def send_notification_mail(self, user_id, email_address, push_action): - raw_from = email.utils.parseaddr(self.notif_from)[1] + def send_notification_mail(self, user_id, email_address, push_actions): + raw_from = email.utils.parseaddr(self.hs.config.email_notif_from)[1] raw_to = email.utils.parseaddr(email_address)[1] if raw_to == '': raise RuntimeError("Invalid 'to' address") - plainText = "yo dawg, you got notifications!" + plainText = self.notif_template.render() text_part = MIMEText(plainText, "plain") text_part['Subject'] = "New Matrix Notifications" - text_part['From'] = self.notif_from + text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address - smtp = smtplib.SMTP(self.smtp_host, self.smtp_port) - smtp.sendmail(raw_from, raw_to, text_part.as_string()) - smtp.quit() + yield sendmail( + self.hs.config.email_smtp_host, + raw_from, raw_to, text_part.as_string(), + port=self.hs.config.email_smtp_port + ) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index b25b736493..a065c78b4d 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -45,6 +45,9 @@ CONDITIONAL_REQUIREMENTS = { "preview_url": { "netaddr>=0.7.18": ["netaddr"], }, + "email.enable_notifs": { + "Jinja2": ["Jinja2"], + }, } -- cgit 1.5.1 From a3ac837599f62b77f458505f841cee6072c1f921 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 21 Apr 2016 17:21:02 +0100 Subject: Optionally split out the pushers into a separate process --- synapse/app/pusher.py | 208 +++++++++++++++++++++++++++++++++++++++++++++ synapse/config/server.py | 1 + synapse/push/httppusher.py | 2 +- synapse/push/pusherpool.py | 4 + synapse/server.py | 3 + 5 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 synapse/app/pusher.py (limited to 'synapse/push') diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py new file mode 100644 index 0000000000..8922573db7 --- /dev/null +++ b/synapse/app/pusher.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import synapse + +from synapse.server import HomeServer +from synapse.util.versionstring import get_version_string +from synapse.config._base import ConfigError +from synapse.config.database import DatabaseConfig +from synapse.config.logger import LoggingConfig +from synapse.replication.slave.storage.events import SlavedEventStore +from synapse.replication.slave.storage.pushers import SlavedPusherStore +from synapse.replication.slave.storage.receipts import SlavedReceiptsStore +from synapse.storage.engines import create_engine +from synapse.storage import DataStore +from synapse.util.async import sleep +from synapse.util.logcontext import (LoggingContext, preserve_fn) + +from twisted.internet import reactor, defer + +import sys +import logging + +logger = logging.getLogger("synapse.app.pusher") + + +class SlaveConfig(DatabaseConfig): + def read_config(self, config): + self.replication_url = config["replication_url"] + self.server_name = config["server_name"] + self.use_insecure_ssl_client_just_for_testing_do_not_use = config.get( + "use_insecure_ssl_client_just_for_testing_do_not_use", False + ) + self.user_agent_suffix = None + self.start_pushers = True + + def default_config(self, **kwargs): + return """\ + ## Slave ## + #replication_url: https://localhost:{replication_port}/_synapse/replication + + report_stats: False + """ + + +class PusherSlaveConfig(SlaveConfig, LoggingConfig): + pass + + +class PusherSlaveStore( + SlavedEventStore, SlavedPusherStore, SlavedReceiptsStore +): + update_pusher_last_stream_ordering_and_success = ( + DataStore.update_pusher_last_stream_ordering_and_success.__func__ + ) + + +class PusherServer(HomeServer): + + def get_db_conn(self, run_new_connection=True): + # Any param beginning with cp_ is a parameter for adbapi, and should + # not be passed to the database engine. + db_params = { + k: v for k, v in self.db_config.get("args", {}).items() + if not k.startswith("cp_") + } + db_conn = self.database_engine.module.connect(**db_params) + + if run_new_connection: + self.database_engine.on_new_connection(db_conn) + return db_conn + + def setup(self): + logger.info("Setting up.") + self.datastore = PusherSlaveStore(self.get_db_conn(), self) + logger.info("Finished setting up.") + + def remove_pusher(self, app_id, push_key, user_id): + http_client = self.get_simple_http_client() + replication_url = self.config.replication_url + url = replication_url + "/remove_pushers" + return http_client.post_json_get_json(url, { + "remove": [{ + "app_id": app_id, + "push_key": push_key, + "user_id": user_id, + }] + }) + + @defer.inlineCallbacks + def replicate(self): + http_client = self.get_simple_http_client() + store = self.get_datastore() + replication_url = self.config.replication_url + pusher_pool = self.get_pusherpool() + + def stop_pusher(user_id, app_id, pushkey): + key = "%s:%s" % (app_id, pushkey) + pushers_for_user = pusher_pool.pushers.get(user_id, {}) + pusher = pushers_for_user.pop(key, None) + if pusher is None: + return + logger.info("Stopping pusher %r / %r", user_id, key) + pusher.on_stop() + + def start_pusher(user_id, app_id, pushkey): + key = "%s:%s" % (app_id, pushkey) + logger.info("Starting pusher %r / %r", user_id, key) + return pusher_pool._refresh_pusher(app_id, pushkey, user_id) + + @defer.inlineCallbacks + def poke_pushers(results): + pushers_rows = set( + map(tuple, results.get("pushers", {}).get("rows", [])) + ) + deleted_pushers_rows = set( + map(tuple, results.get("deleted_pushers", {}).get("rows", [])) + ) + for row in sorted(pushers_rows | deleted_pushers_rows): + if row in deleted_pushers_rows: + user_id, app_id, pushkey = row[1:4] + stop_pusher(user_id, app_id, pushkey) + elif row in pushers_rows: + user_id = row[1] + app_id = row[5] + pushkey = row[8] + yield start_pusher(user_id, app_id, pushkey) + + stream = results.get("events") + if stream: + min_stream_id = stream["rows"][0][0] + max_stream_id = stream["position"] + preserve_fn(pusher_pool.on_new_notifications)( + min_stream_id, max_stream_id + ) + + stream = results.get("receipts") + if stream: + rows = stream["rows"] + affected_room_ids = set(row[1] for row in rows) + min_stream_id = rows[0][0] + max_stream_id = stream["position"] + preserve_fn(pusher_pool.on_new_receipts)( + min_stream_id, max_stream_id, affected_room_ids + ) + + while True: + try: + args = store.stream_positions() + args["timeout"] = 30000 + result = yield http_client.get_json(replication_url, args=args) + yield store.process_replication(result) + poke_pushers(result) + except: + logger.exception("Error replicating from %r", replication_url) + sleep(30) + + +def setup(config_options): + try: + config = PusherSlaveConfig.load_config( + "Synapse pusher", config_options + ) + except ConfigError as e: + sys.stderr.write("\n" + e.message + "\n") + sys.exit(1) + + config.setup_logging() + + database_engine = create_engine(config.database_config) + + ps = PusherServer( + config.server_name, + db_config=config.database_config, + config=config, + version_string=get_version_string("Synapse", synapse), + database_engine=database_engine, + ) + + ps.setup() + + def start(): + ps.replicate() + ps.get_pusherpool().start() + ps.get_datastore().start_profiling() + + reactor.callWhenRunning(start) + + return ps + + +if __name__ == '__main__': + with LoggingContext("main"): + ps = setup(sys.argv[1:]) + reactor.run() diff --git a/synapse/config/server.py b/synapse/config/server.py index df4707e1d1..46c633548a 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -28,6 +28,7 @@ class ServerConfig(Config): self.print_pidfile = config.get("print_pidfile") self.user_agent_suffix = config.get("user_agent_suffix") self.use_frozen_dicts = config.get("use_frozen_dicts", True) + self.start_pushers = config.get("start_pushers", True) self.listeners = config.get("listeners", []) diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 6950a20632..3992804845 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -230,7 +230,7 @@ class HttpPusher(object): "Pushkey %s was rejected: removing", pk ) - yield self.hs.get_pusherpool().remove_pusher( + yield self.hs.remove_pusher( self.app_id, pk, self.user_id ) defer.returnValue(True) diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index aa095f9d9b..6ef48d63f7 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -29,6 +29,7 @@ logger = logging.getLogger(__name__) class PusherPool: def __init__(self, _hs): self.hs = _hs + self.start_pushers = _hs.config.start_pushers self.store = self.hs.get_datastore() self.clock = self.hs.get_clock() self.pushers = {} @@ -177,6 +178,9 @@ class PusherPool: self._start_pushers([p]) def _start_pushers(self, pushers): + if not self.start_pushers: + logger.info("Not starting pushers because they are disabled in the config") + return logger.info("Starting %d pushers", len(pushers)) for pusherdict in pushers: try: diff --git a/synapse/server.py b/synapse/server.py index 368d615576..ee138de756 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -193,6 +193,9 @@ class HomeServer(object): **self.db_config.get("args", {}) ) + def remove_pusher(self, app_id, push_key, user_id): + return self.get_pusherpool().remove_pusher(app_id, push_key, user_id) + def _make_dependency_method(depname): def _get(hs): -- cgit 1.5.1 From c10ed26c303741fe0e43f11e2fbeeb148f466b17 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Apr 2016 19:19:07 +0100 Subject: Flesh out email templating Mostly WIP porting the room name calculation logic from the web client so our room names in the email mirror the clients. --- synapse/push/emailpusher.py | 7 ++ synapse/push/mailer.py | 61 +++++++++++++++++- synapse/python_dependencies.py | 2 +- synapse/util/room_name.py | 142 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 synapse/util/room_name.py (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 4e21221fb7..7c810029fa 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -82,6 +82,13 @@ class EmailPusher(object): self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) yield self._process() + @defer.inlineCallbacks + def on_new_receipts(self, min_stream_id, max_stream_id): + # We could wake up and cancel the timer but there tend to be quite a + # lot of read receipts so it's probably less work to just let the + # timer fire + return defer.succeed(None) + @defer.inlineCallbacks def on_timer(self): self.timed_call = None diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 0f20d43f75..e68d701ffd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -14,18 +14,23 @@ # limitations under the License. from twisted.internet import defer - from twisted.mail.smtp import sendmail + import email.utils import email.mime.multipart from email.mime.text import MIMEText +from synapse.util.async import concurrently_execute +from synapse.util.room_name import calculate_room_name + import jinja2 class Mailer(object): def __init__(self, hs): self.hs = hs + self.store = self.hs.get_datastore() + self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) env = jinja2.Environment(loader=loader) self.notif_template = env.get_template(self.hs.config.email_notif_template_html) @@ -38,9 +43,41 @@ class Mailer(object): if raw_to == '': raise RuntimeError("Invalid 'to' address") - plainText = self.notif_template.render() + rooms_in_order = deduped_ordered_list( + [pa['room_id'] for pa in push_actions] + ) + + notifs_by_room = {} + for pa in push_actions: + notifs_by_room.setdefault(pa["room_id"], []).append(pa) + + # collect the current state for all the rooms in which we have + # notifications + state_by_room = {} + + @defer.inlineCallbacks + def _fetch_room_state(room_id): + room_state = yield self.state_handler.get_current_state(room_id) + state_by_room[room_id] = room_state + + # Run at most 3 of these at once: sync does 10 at a time but email + # notifs are much realtime than sync so we can afford to wait a bit. + yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) - text_part = MIMEText(plainText, "plain") + rooms = [ + self.get_room_vars( + r, user_id, notifs_by_room[r], state_by_room[r] + ) for r in rooms_in_order + ] + + template_vars = { + "unsubscribe_link": self.make_unsubscribe_link(), + "rooms": rooms, + } + + plainText = self.notif_template.render(**template_vars) + + text_part = MIMEText(plainText, "html") text_part['Subject'] = "New Matrix Notifications" text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address @@ -50,3 +87,21 @@ class Mailer(object): raw_from, raw_to, text_part.as_string(), port=self.hs.config.email_smtp_port ) + + def get_room_vars(self, room_id, user_id, notifs, room_state): + room_vars = {} + room_vars['title'] = calculate_room_name(room_state, user_id) + return room_vars + + def make_unsubscribe_link(self): + return "https://vector.im/#/settings" # XXX: matrix.to + + +def deduped_ordered_list(l): + seen = set() + ret = [] + for item in l: + if item not in seen: + seen.add(item) + ret.append(item) + return ret \ No newline at end of file diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index a065c78b4d..16524dbdcd 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -46,7 +46,7 @@ CONDITIONAL_REQUIREMENTS = { "netaddr>=0.7.18": ["netaddr"], }, "email.enable_notifs": { - "Jinja2": ["Jinja2"], + "Jinja2>=2.8": ["Jinja2>=2.8"], }, } diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py new file mode 100644 index 0000000000..7e49b92bb4 --- /dev/null +++ b/synapse/util/room_name.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +# intentionally looser than what aliases we allow to be registered since +# other HSes may allow aliases that we would not +ALIAS_RE = re.compile(r"^#.*:.+$") + +ALL_ALONE = "Empty Room" + + +def calculate_room_name(room_state, user_id): + # does it have a name? + if ("m.room.name", "") in room_state: + m_room_name = room_state[("m.room.name", "")] + if m_room_name.content and m_room_name.content["name"]: + return m_room_name.content["name"] + + # does it have a caononical alias? + if ("m.room.canonical_alias", "") in room_state: + canon_alias = room_state[("m.room.canonical_alias", "")] + if ( + canon_alias.content and canon_alias.content["alias"] and + looks_like_an_alias(canon_alias.content["alias"]) + ): + return canon_alias.content["alias"] + + # at this point we're going to need to search the state by all state keys + # for an event type, so rearrange the data structure + room_state_bytype = state_as_two_level_dict(room_state) + + # right then, any aliases at all? + if "m.room.aliases" in room_state_bytype: + m_room_aliases = room_state_bytype["m.room.aliases"] + if len(m_room_aliases.values()) > 0: + first_alias_event = m_room_aliases.values()[0] + if first_alias_event.content and first_alias_event.content["aliases"]: + the_aliases = first_alias_event.content["aliases"] + if len(the_aliases) > 0 and looks_like_an_alias(the_aliases[0]): + return the_aliases[0] + + my_member_event = None + if ("m.room.member", user_id) in room_state: + my_member_event = room_state[("m.room.member", user_id)] + + if ( + my_member_event is not None and + my_member_event.content['membership'] == "invite" + ): + if ("m.room.member", my_member_event.sender) in room_state: + inviter_member_event = room_state[("m.room.member", my_member_event.sender)] + return "Invite from %s" % (name_from_member_event(inviter_member_event),) + else: + return "Room Invite" + + # we're going to have to generate a name based on who's in the room, + # so find out who is in the room that isn't the user. + if "m.room.member" in room_state_bytype: + all_members = [ + ev for ev in room_state_bytype["m.room.member"].values() + if ev.membership == "join" or ev.membership == "invite" + ] + other_members = [m for m in all_members if m.sender != user_id] + else: + other_members = [] + all_members = [] + + if len(other_members) == 0: + if len(all_members) == 1: + # self-chat, peeked room with 1 participant, + # or inbound invite, or outbound 3PID invite. + if all_members[0].sender == user_id: + if "m.room.third_party_invite" in room_state_bytype: + third_party_invites = room_state_bytype["m.room.third_party_invite"] + if len(third_party_invites) > 0: + # technically third party invite events are not member + # events, but they are close enough + return "Inviting %s" ( + descriptor_from_member_events(third_party_invites) + ) + else: + return ALL_ALONE + else: + return name_from_member_event(all_members[0]) + else: + return ALL_ALONE + else: + return descriptor_from_member_events(other_members) + + +def state_as_two_level_dict(state): + ret = {} + for k, v in state.items(): + ret.setdefault(k[0], {})[k[1]] = v + return ret + + +def looks_like_an_alias(string): + return ALIAS_RE.match(string) is not None + + +def descriptor_from_member_events(member_events): + # else if (otherMembers.length === 1) { + # return otherMembers[0].name; + # } + # else if (otherMembers.length === 2) { + # return ( + # otherMembers[0].name + " and " + otherMembers[1].name + # ); + # } + # else { + # return ( + # otherMembers[0].name + " and " + (otherMembers.length - 1) + " others" + # ); + # } + if len(member_events) == 0: + return "nobody" + elif len(member_events) == 1: + return name_from_member_event(member_events[0]) + return "all the people, so many people. They all go hand in hand, hand in hand in their park life." + + +def name_from_member_event(member_event): + if ( + member_event.content and "displayname" in member_event.content and + member_event.content["displayname"] + ): + return member_event.content["displayname"] + return member_event.sender \ No newline at end of file -- cgit 1.5.1 From c553797c4f0b772cfa9c9370d0789bf32d82e6c5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 17:27:54 +0100 Subject: No inlineCallbacks necessary on this --- synapse/push/emailpusher.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 7c810029fa..dcbee4c3fe 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -82,7 +82,6 @@ class EmailPusher(object): self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) yield self._process() - @defer.inlineCallbacks def on_new_receipts(self, min_stream_id, max_stream_id): # We could wake up and cancel the timer but there tend to be quite a # lot of read receipts so it's probably less work to just let the -- cgit 1.5.1 From 83bf65297a624a205ce13e27f0f7c887b7db48af Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 18:31:47 +0100 Subject: Mime part is binary so encode it first. Doesn't get character enocind right yet but makes it not error. --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e68d701ffd..f679718b02 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -77,7 +77,7 @@ class Mailer(object): plainText = self.notif_template.render(**template_vars) - text_part = MIMEText(plainText, "html") + text_part = MIMEText(plainText.encode('utf8'), "html") text_part['Subject'] = "New Matrix Notifications" text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address -- cgit 1.5.1 From bd0f9c2065a0f766786493649893489b8e0e3d38 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 18:42:00 +0100 Subject: Actually do UTF8 correctly --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index f679718b02..9212d36b84 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -77,7 +77,7 @@ class Mailer(object): plainText = self.notif_template.render(**template_vars) - text_part = MIMEText(plainText.encode('utf8'), "html") + text_part = MIMEText(plainText, "html", "utf8") text_part['Subject'] = "New Matrix Notifications" text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address -- cgit 1.5.1 From 7b4715bad704231b51c6d0462cfd19ed32df5e0b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Apr 2016 18:27:04 +0100 Subject: More variable calculation for email notifs Include name of the person we're sending to and add summary text at the top giving an overview of what's happened. --- res/templates/notif.html | 3 +- synapse/push/mailer.py | 57 ++++++++++++++- synapse/util/presentable_names.py | 145 ++++++++++++++++++++++++++++++++++++++ synapse/util/room_name.py | 142 ------------------------------------- 4 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 synapse/util/presentable_names.py delete mode 100644 synapse/util/room_name.py (limited to 'synapse/push') diff --git a/res/templates/notif.html b/res/templates/notif.html index 648ff034b3..aee52ec8c9 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -1,7 +1,8 @@ -

{{ summaryText }}

+
Hi {{ user_display_name }},
+
{{ summary_text }}
{% for room in rooms %} {% include 'room.html' with context %} diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 9212d36b84..9e2297a03b 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -21,11 +21,19 @@ import email.mime.multipart from email.mime.text import MIMEText from synapse.util.async import concurrently_execute -from synapse.util.room_name import calculate_room_name +from synapse.util.presentable_names import calculate_room_name, name_from_member_event +from synapse.types import UserID +from synapse.api.errors import StoreError import jinja2 +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" +MESSAGE_FROM_PERSON = "You have a message from %s" +MESSAGES_IN_ROOM = "There are some messages for you in the %s room" +MESSAGES_IN_ROOMS = "Here are some messages you may have missed" + + class Mailer(object): def __init__(self, hs): self.hs = hs @@ -55,6 +63,13 @@ class Mailer(object): # notifications state_by_room = {} + try: + user_display_name = yield self.store.get_profile_displayname( + UserID.from_string(user_id).localpart + ) + except StoreError: + user_display_name = user_id + @defer.inlineCallbacks def _fetch_room_state(room_id): room_state = yield self.state_handler.get_current_state(room_id) @@ -70,8 +85,14 @@ class Mailer(object): ) for r in rooms_in_order ] + summary_text = yield self.make_summary_text( + notifs_by_room, state_by_room, user_id + ) + template_vars = { + "user_display_name": user_display_name, "unsubscribe_link": self.make_unsubscribe_link(), + "summary_text": summary_text, "rooms": rooms, } @@ -93,6 +114,38 @@ class Mailer(object): room_vars['title'] = calculate_room_name(room_state, user_id) return room_vars + @defer.inlineCallbacks + def make_summary_text(self, notifs_by_room, state_by_room, user_id): + if len(notifs_by_room) == 1: + room_id = notifs_by_room.keys()[0] + sender_name = None + if len(notifs_by_room[room_id]) == 1: + # If the room has some kind of name, use it, but we don't + # want the generated-from-names one here otherwise we'll + # end up with, "new message from Bob in the Bob room" + room_name = calculate_room_name( + state_by_room[room_id], user_id, fallback_to_members=False + ) + event = yield self.store.get_event( + notifs_by_room[room_id][0]["event_id"] + ) + if ("m.room.member", event.sender) in state_by_room[room_id]: + state_event = state_by_room[room_id][("m.room.member", event.sender)] + sender_name = name_from_member_event(state_event) + if sender_name is not None and room_name is not None: + defer.returnValue( + MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) + ) + elif sender_name is not None: + defer.returnValue(MESSAGE_FROM_PERSON % (sender_name,)) + else: + room_name = calculate_room_name(state_by_room[room_id], user_id) + defer.returnValue(MESSAGES_IN_ROOM % (room_name,)) + else: + defer.returnValue(MESSAGES_IN_ROOMS) + + defer.returnValue("Some thing have occurred in some rooms") + def make_unsubscribe_link(self): return "https://vector.im/#/settings" # XXX: matrix.to @@ -104,4 +157,4 @@ def deduped_ordered_list(l): if item not in seen: seen.add(item) ret.append(item) - return ret \ No newline at end of file + return ret diff --git a/synapse/util/presentable_names.py b/synapse/util/presentable_names.py new file mode 100644 index 0000000000..2ae01e453d --- /dev/null +++ b/synapse/util/presentable_names.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +# intentionally looser than what aliases we allow to be registered since +# other HSes may allow aliases that we would not +ALIAS_RE = re.compile(r"^#.*:.+$") + +ALL_ALONE = "Empty Room" + + +def calculate_room_name(room_state, user_id, fallback_to_members=True): + # does it have a name? + if ("m.room.name", "") in room_state: + m_room_name = room_state[("m.room.name", "")] + if m_room_name.content and m_room_name.content["name"]: + return m_room_name.content["name"] + + # does it have a canonical alias? + if ("m.room.canonical_alias", "") in room_state: + canon_alias = room_state[("m.room.canonical_alias", "")] + if ( + canon_alias.content and canon_alias.content["alias"] and + _looks_like_an_alias(canon_alias.content["alias"]) + ): + return canon_alias.content["alias"] + + # at this point we're going to need to search the state by all state keys + # for an event type, so rearrange the data structure + room_state_bytype = _state_as_two_level_dict(room_state) + + # right then, any aliases at all? + if "m.room.aliases" in room_state_bytype: + m_room_aliases = room_state_bytype["m.room.aliases"] + if len(m_room_aliases.values()) > 0: + first_alias_event = m_room_aliases.values()[0] + if first_alias_event.content and first_alias_event.content["aliases"]: + the_aliases = first_alias_event.content["aliases"] + if len(the_aliases) > 0 and _looks_like_an_alias(the_aliases[0]): + return the_aliases[0] + + my_member_event = None + if ("m.room.member", user_id) in room_state: + my_member_event = room_state[("m.room.member", user_id)] + + if ( + my_member_event is not None and + my_member_event.content['membership'] == "invite" + ): + if ("m.room.member", my_member_event.sender) in room_state: + inviter_member_event = room_state[("m.room.member", my_member_event.sender)] + return "Invite from %s" % (name_from_member_event(inviter_member_event),) + else: + return "Room Invite" + + if not fallback_to_members: + return None + + # we're going to have to generate a name based on who's in the room, + # so find out who is in the room that isn't the user. + if "m.room.member" in room_state_bytype: + all_members = [ + ev for ev in room_state_bytype["m.room.member"].values() + if ev.content['membership'] == "join" or ev.content['membership'] == "invite" + ] + # Sort the member events oldest-first so the we name people in the + # order the joined (it should at least be deterministic rather than + # dictionary iteration order) + all_members.sort(key=lambda e: e.origin_server_ts) + other_members = [m for m in all_members if m.state_key != user_id] + else: + other_members = [] + all_members = [] + + if len(other_members) == 0: + if len(all_members) == 1: + # self-chat, peeked room with 1 participant, + # or inbound invite, or outbound 3PID invite. + if all_members[0].sender == user_id: + if "m.room.third_party_invite" in room_state_bytype: + third_party_invites = room_state_bytype["m.room.third_party_invite"] + if len(third_party_invites) > 0: + # technically third party invite events are not member + # events, but they are close enough + return "Inviting %s" ( + descriptor_from_member_events(third_party_invites) + ) + else: + return ALL_ALONE + else: + return name_from_member_event(all_members[0]) + else: + return ALL_ALONE + else: + return descriptor_from_member_events(other_members) + + +def descriptor_from_member_events(member_events): + if len(member_events) == 0: + return "nobody" + elif len(member_events) == 1: + return name_from_member_event(member_events[0]) + elif len(member_events) == 2: + return "%s and %s" % ( + name_from_member_event(member_events[0]), + name_from_member_event(member_events[1]), + ) + else: + return "%s and %d others" % ( + name_from_member_event(member_events[0]), + len(member_events) - 1, + ) + + +def name_from_member_event(member_event): + if ( + member_event.content and "displayname" in member_event.content and + member_event.content["displayname"] + ): + return member_event.content["displayname"] + return member_event.state_key + + +def _state_as_two_level_dict(state): + ret = {} + for k, v in state.items(): + ret.setdefault(k[0], {})[k[1]] = v + return ret + + +def _looks_like_an_alias(string): + return ALIAS_RE.match(string) is not None \ No newline at end of file diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py deleted file mode 100644 index f55ef293b6..0000000000 --- a/synapse/util/room_name.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -# intentionally looser than what aliases we allow to be registered since -# other HSes may allow aliases that we would not -ALIAS_RE = re.compile(r"^#.*:.+$") - -ALL_ALONE = "Empty Room" - - -def calculate_room_name(room_state, user_id): - # does it have a name? - if ("m.room.name", "") in room_state: - m_room_name = room_state[("m.room.name", "")] - if m_room_name.content and m_room_name.content["name"]: - return m_room_name.content["name"] - - # does it have a canonical alias? - if ("m.room.canonical_alias", "") in room_state: - canon_alias = room_state[("m.room.canonical_alias", "")] - if ( - canon_alias.content and canon_alias.content["alias"] and - looks_like_an_alias(canon_alias.content["alias"]) - ): - return canon_alias.content["alias"] - - # at this point we're going to need to search the state by all state keys - # for an event type, so rearrange the data structure - room_state_bytype = state_as_two_level_dict(room_state) - - # right then, any aliases at all? - if "m.room.aliases" in room_state_bytype: - m_room_aliases = room_state_bytype["m.room.aliases"] - if len(m_room_aliases.values()) > 0: - first_alias_event = m_room_aliases.values()[0] - if first_alias_event.content and first_alias_event.content["aliases"]: - the_aliases = first_alias_event.content["aliases"] - if len(the_aliases) > 0 and looks_like_an_alias(the_aliases[0]): - return the_aliases[0] - - my_member_event = None - if ("m.room.member", user_id) in room_state: - my_member_event = room_state[("m.room.member", user_id)] - - if ( - my_member_event is not None and - my_member_event.content['membership'] == "invite" - ): - if ("m.room.member", my_member_event.sender) in room_state: - inviter_member_event = room_state[("m.room.member", my_member_event.sender)] - return "Invite from %s" % (name_from_member_event(inviter_member_event),) - else: - return "Room Invite" - - # we're going to have to generate a name based on who's in the room, - # so find out who is in the room that isn't the user. - if "m.room.member" in room_state_bytype: - all_members = [ - ev for ev in room_state_bytype["m.room.member"].values() - if ev.content['membership'] == "join" or ev.content['membership'] == "invite" - ] - # Sort the member events oldest-first so the we name people in the - # order the joined (it should at least be deterministic rather than - # dictionary iteration order) - all_members.sort(key=lambda e: e.origin_server_ts) - other_members = [m for m in all_members if m.state_key != user_id] - else: - other_members = [] - all_members = [] - - if len(other_members) == 0: - if len(all_members) == 1: - # self-chat, peeked room with 1 participant, - # or inbound invite, or outbound 3PID invite. - if all_members[0].sender == user_id: - if "m.room.third_party_invite" in room_state_bytype: - third_party_invites = room_state_bytype["m.room.third_party_invite"] - if len(third_party_invites) > 0: - # technically third party invite events are not member - # events, but they are close enough - return "Inviting %s" ( - descriptor_from_member_events(third_party_invites) - ) - else: - return ALL_ALONE - else: - return name_from_member_event(all_members[0]) - else: - return ALL_ALONE - else: - return descriptor_from_member_events(other_members) - - -def state_as_two_level_dict(state): - ret = {} - for k, v in state.items(): - ret.setdefault(k[0], {})[k[1]] = v - return ret - - -def looks_like_an_alias(string): - return ALIAS_RE.match(string) is not None - - -def descriptor_from_member_events(member_events): - if len(member_events) == 0: - return "nobody" - elif len(member_events) == 1: - return name_from_member_event(member_events[0]) - elif len(member_events) == 2: - return "%s and %s" % ( - name_from_member_event(member_events[0]), - name_from_member_event(member_events[1]), - ) - else: - return "%s and %d others" % ( - name_from_member_event(member_events[0]), - len(member_events) - 1, - ) - - -def name_from_member_event(member_event): - if ( - member_event.content and "displayname" in member_event.content and - member_event.content["displayname"] - ): - return member_event.content["displayname"] - return member_event.state_key \ No newline at end of file -- cgit 1.5.1 From fa12209c1b297a1710f487744a8a143d6cb6a2d1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 15:09:55 +0100 Subject: Hopefully all remaining bits for email notifs Add public facing base url to the server so synapse knows what URL to use when converting mxc to http urls for use in emails --- res/templates/notif.html | 15 ---- res/templates/notif_mail.html | 15 ++++ res/templates/room.html | 23 +++++- synapse/config/emailconfig.py | 9 ++- synapse/config/server.py | 8 ++ synapse/push/mailer.py | 166 +++++++++++++++++++++++++++++++++++------ synapse/python_dependencies.py | 1 + 7 files changed, 195 insertions(+), 42 deletions(-) delete mode 100644 res/templates/notif.html create mode 100644 res/templates/notif_mail.html (limited to 'synapse/push') diff --git a/res/templates/notif.html b/res/templates/notif.html deleted file mode 100644 index aee52ec8c9..0000000000 --- a/res/templates/notif.html +++ /dev/null @@ -1,15 +0,0 @@ - - - -
Hi {{ user_display_name }},
-
{{ summary_text }}
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
- - - diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html new file mode 100644 index 0000000000..fbfb0a767c --- /dev/null +++ b/res/templates/notif_mail.html @@ -0,0 +1,15 @@ + + + +
Hi {{ user_display_name }},
+
{{ summary_text }}
+
+ {% for room in rooms %} + {% include 'room.html' with context %} + {% endfor %} +
+ + + diff --git a/res/templates/room.html b/res/templates/room.html index ef36b4ee58..f369575b98 100644 --- a/res/templates/room.html +++ b/res/templates/room.html @@ -1,6 +1,21 @@
-

{{ room.title }}

-
- Things have happened in this room -
+

{{ room.title }}

+
+ {% if room.avatar_url %} + + {% else %} + {% if room.hash % 3 == 0 %} + + {% elif room.hash % 3 == 1 %} + + {% else %} + + {% endif %} + {% endif %} +
+
+ {% for notif in room.notifs %} + {% include 'notif.html' with context %} + {% endfor %} +
diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 68fb4d8060..893034e2ef 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -25,17 +25,19 @@ class EmailConfig(Config): """ def read_config(self, config): + self.email_enable_notifs = False + email_config = config.get("email", None) if email_config: self.email_enable_notifs = email_config.get("enable_notifs", True) + if self.email_enable_notifs: required = [ "smtp_host", "smtp_port", "notif_from", "template_dir", "notif_template_html", - ] missing = [] @@ -49,6 +51,11 @@ class EmailConfig(Config): (", ".join(["email."+k for k in missing]),) ) + if config.get("public_baseurl") is None: + raise RuntimeError( + "email.enable_notifs is True but no public_baseurl is set" + ) + self.email_smtp_host = email_config["smtp_host"] self.email_smtp_port = email_config["smtp_port"] self.email_notif_from = email_config["notif_from"] diff --git a/synapse/config/server.py b/synapse/config/server.py index df4707e1d1..19af39da70 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -28,6 +28,11 @@ class ServerConfig(Config): self.print_pidfile = config.get("print_pidfile") self.user_agent_suffix = config.get("user_agent_suffix") self.use_frozen_dicts = config.get("use_frozen_dicts", True) + self.public_baseurl = config.get("public_baseurl") + + if self.public_baseurl is not None: + if self.public_baseurl[-1] != '/': + self.public_baseurl += '/' self.listeners = config.get("listeners", []) @@ -142,6 +147,9 @@ class ServerConfig(Config): # Whether to serve a web client from the HTTP/HTTPS root resource. web_client: True + # The server's public-facing base URL + # https://example.com:8448/ + # Set the soft limit on the number of file descriptors synapse can use # Zero is used to indicate synapse should set the soft limit to the # hard limit. diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 9e2297a03b..e78c26edea 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -26,6 +26,10 @@ from synapse.types import UserID from synapse.api.errors import StoreError import jinja2 +import bleach + +import time +import urllib MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" @@ -33,6 +37,27 @@ MESSAGE_FROM_PERSON = "You have a message from %s" MESSAGES_IN_ROOM = "There are some messages for you in the %s room" MESSAGES_IN_ROOMS = "Here are some messages you may have missed" +CONTEXT_BEFORE = 1 + +# From https://github.com/matrix-org/matrix-react-sdk/blob/master/src/HtmlUtils.js +ALLOWED_TAGS = [ + 'font', # custom to matrix for IRC-style font coloring + 'del', # for markdown + # deliberately no h1/h2 to stop people shouting. + 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', + 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', + 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre' +] +ALLOWED_ATTRS = { + # custom ones first: + "font": ["color"], # custom to matrix + "a": ["href", "name", "target"], # remote target: custom to matrix + # We don't currently allow img itself by default, but this + # would make sense if we did + "img": ["src"], +} +ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] + class Mailer(object): def __init__(self, hs): @@ -41,6 +66,8 @@ class Mailer(object): self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) env = jinja2.Environment(loader=loader) + env.filters["format_ts"] = format_ts_filter + env.filters["mxc_to_http"] = self.mxc_to_http_filter self.notif_template = env.get_template(self.hs.config.email_notif_template_html) @defer.inlineCallbacks @@ -55,6 +82,10 @@ class Mailer(object): [pa['room_id'] for pa in push_actions] ) + notif_events = yield self.store.get_events( + [pa['event_id'] for pa in push_actions] + ) + notifs_by_room = {} for pa in push_actions: notifs_by_room.setdefault(pa["room_id"], []).append(pa) @@ -79,14 +110,16 @@ class Mailer(object): # notifs are much realtime than sync so we can afford to wait a bit. yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) - rooms = [ - self.get_room_vars( - r, user_id, notifs_by_room[r], state_by_room[r] - ) for r in rooms_in_order - ] + rooms = [] - summary_text = yield self.make_summary_text( - notifs_by_room, state_by_room, user_id + for r in rooms_in_order: + vars = yield self.get_room_vars( + r, user_id, notifs_by_room[r], notif_events, state_by_room[r] + ) + rooms.append(vars) + + summary_text = self.make_summary_text( + notifs_by_room, state_by_room, notif_events, user_id ) template_vars = { @@ -109,13 +142,72 @@ class Mailer(object): port=self.hs.config.email_smtp_port ) - def get_room_vars(self, room_id, user_id, notifs, room_state): - room_vars = {} - room_vars['title'] = calculate_room_name(room_state, user_id) - return room_vars + @defer.inlineCallbacks + def get_room_vars(self, room_id, user_id, notifs, notif_events, room_state): + room_vars = { + "title": calculate_room_name(room_state, user_id), + "hash": string_ordinal_total(room_id), # See sender avatar hash + "notifs": [], + } + + for n in notifs: + vars = yield self.get_notif_vars(n, notif_events[n['event_id']], room_state) + room_vars['notifs'].append(vars) + + defer.returnValue(room_vars) @defer.inlineCallbacks - def make_summary_text(self, notifs_by_room, state_by_room, user_id): + def get_notif_vars(self, notif, notif_event, room_state): + results = yield self.store.get_events_around( + notif['room_id'], notif['event_id'], + before_limit=CONTEXT_BEFORE, after_limit=0 + ) + + ret = { + "link": self.make_notif_link(notif), + "ts": notif['received_ts'], + "messages": [], + } + + for event in results['events_before']: + vars = self.get_message_vars(notif, event, room_state) + if vars is not None: + ret['messages'].append(vars) + + vars = self.get_message_vars(notif, notif_event, room_state) + if vars is not None: + ret['messages'].append(vars) + + defer.returnValue(ret) + + def get_message_vars(self, notif, event, room_state): + msgtype = event.content["msgtype"] + + sender_state_event = room_state[("m.room.member", event.sender)] + sender_name = name_from_member_event(sender_state_event) + sender_avatar_url = sender_state_event.content["avatar_url"] + + # 'hash' for deterministically picking default images: use + # sender_hash % the number of default images to choose from + sender_hash = string_ordinal_total(event.sender) + + ret = { + "msgtype": msgtype, + "is_historical": event.event_id != notif['event_id'], + "ts": event.origin_server_ts, + "sender_name": sender_name, + "sender_avatar_url": sender_avatar_url, + "sender_hash": sender_hash, + } + + if msgtype == "m.text": + ret["body_text_plain"] = event.content["body"] + elif msgtype == "org.matrix.custom.html": + ret["body_text_html"] = safe_markup(event.content["formatted_body"]) + + return ret + + def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: room_id = notifs_by_room.keys()[0] sender_name = None @@ -126,29 +218,50 @@ class Mailer(object): room_name = calculate_room_name( state_by_room[room_id], user_id, fallback_to_members=False ) - event = yield self.store.get_event( - notifs_by_room[room_id][0]["event_id"] - ) + event = notif_events[notifs_by_room[room_id][0]["event_id"]] if ("m.room.member", event.sender) in state_by_room[room_id]: state_event = state_by_room[room_id][("m.room.member", event.sender)] sender_name = name_from_member_event(state_event) if sender_name is not None and room_name is not None: - defer.returnValue( - MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) - ) + return MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) elif sender_name is not None: - defer.returnValue(MESSAGE_FROM_PERSON % (sender_name,)) + return MESSAGE_FROM_PERSON % (sender_name,) else: room_name = calculate_room_name(state_by_room[room_id], user_id) - defer.returnValue(MESSAGES_IN_ROOM % (room_name,)) + return MESSAGES_IN_ROOM % (room_name,) else: - defer.returnValue(MESSAGES_IN_ROOMS) + return MESSAGES_IN_ROOMS - defer.returnValue("Some thing have occurred in some rooms") + def make_notif_link(self, notif): + return "https://matrix.to/%s/%s" % ( + notif['room_id'], notif['event_id'] + ) def make_unsubscribe_link(self): return "https://vector.im/#/settings" # XXX: matrix.to + def mxc_to_http_filter(self, value, width, height, resizeMethod="crop"): + if value[0:6] != "mxc://": + return "" + serverAndMediaId = value[6:] + params = { + "width": width, + "height": height, + "method": resizeMethod, + } + return "%s_matrix/media/v1/thumbnail/%s?%s" % ( + self.hs.config.public_baseurl, + serverAndMediaId, + urllib.urlencode(params) + ) + + +def safe_markup(self, raw_html): + return jinja2.Markup(bleach.linkify(bleach.clean( + raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, + protocols=ALLOWED_SCHEMES, strip=True + ))) + def deduped_ordered_list(l): seen = set() @@ -158,3 +271,12 @@ def deduped_ordered_list(l): seen.add(item) ret.append(item) return ret + +def string_ordinal_total(s): + tot = 0 + for c in s: + tot += ord(c) + return tot + +def format_ts_filter(value, format): + return time.strftime(format, time.localtime(value / 1000)) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 16524dbdcd..618f3c43ab 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -47,6 +47,7 @@ CONDITIONAL_REQUIREMENTS = { }, "email.enable_notifs": { "Jinja2>=2.8": ["Jinja2>=2.8"], + "bleach>=1.4.2": ["bleach>=1.4.2"], }, } -- cgit 1.5.1 From 8781083960325e7aae16b1745c8e90561fde3d35 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 15:30:41 +0100 Subject: Better grammar for multiple messages in a room Say who the messages are from if there's no room name, otherwise it's a bit nonsensical --- synapse/push/emailpusher.py | 2 +- synapse/push/mailer.py | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 10 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index dcbee4c3fe..6ae16e9865 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification # (to give the user a chance to respond to other push or notice the window) -DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 +DELAY_BEFORE_MAIL_MS = 2000#2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e78c26edea..272e372beb 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -21,7 +21,9 @@ import email.mime.multipart from email.mime.text import MIMEText from synapse.util.async import concurrently_execute -from synapse.util.presentable_names import calculate_room_name, name_from_member_event +from synapse.util.presentable_names import ( + calculate_room_name, name_from_member_event, descriptor_from_member_events +) from synapse.types import UserID from synapse.api.errors import StoreError @@ -34,6 +36,7 @@ import urllib MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" MESSAGE_FROM_PERSON = "You have a message from %s" +MESSAGES_FROM_PERSON = "You have messages from %s" MESSAGES_IN_ROOM = "There are some messages for you in the %s room" MESSAGES_IN_ROOMS = "Here are some messages you may have missed" @@ -209,15 +212,19 @@ class Mailer(object): def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: + # Only one room has new stuff room_id = notifs_by_room.keys()[0] + + # If the room has some kind of name, use it, but we don't + # want the generated-from-names one here otherwise we'll + # end up with, "new message from Bob in the Bob room" + room_name = calculate_room_name( + state_by_room[room_id], user_id, fallback_to_members=False + ) + sender_name = None if len(notifs_by_room[room_id]) == 1: - # If the room has some kind of name, use it, but we don't - # want the generated-from-names one here otherwise we'll - # end up with, "new message from Bob in the Bob room" - room_name = calculate_room_name( - state_by_room[room_id], user_id, fallback_to_members=False - ) + # There is just the one notification, so give some detail event = notif_events[notifs_by_room[room_id][0]["event_id"]] if ("m.room.member", event.sender) in state_by_room[room_id]: state_event = state_by_room[room_id][("m.room.member", event.sender)] @@ -227,9 +234,25 @@ class Mailer(object): elif sender_name is not None: return MESSAGE_FROM_PERSON % (sender_name,) else: - room_name = calculate_room_name(state_by_room[room_id], user_id) - return MESSAGES_IN_ROOM % (room_name,) + # There's more than one notification for this room, so just + # say there are several + if room_name is not None: + return MESSAGES_IN_ROOM % (room_name,) + else: + # If the room doesn't have a name, say who the messages + # are from explicitly to avoid, "messages in the Bob room" + sender_ids = list(set([ + notif_events[n['event_id']].sender + for n in notifs_by_room[room_id] + ])) + + return MESSAGES_FROM_PERSON % ( + descriptor_from_member_events([ + state_by_room[room_id][("m.room.member", s)] for s in sender_ids + ]) + ) else: + # Stuff's happened in multiple different rooms return MESSAGES_IN_ROOMS def make_notif_link(self, notif): -- cgit 1.5.1 From 4ed1e45869cf59b517df38831a373a1a770e5917 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 17:18:51 +0100 Subject: Make html messages work --- synapse/push/mailer.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 272e372beb..a4a0891e05 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -59,7 +59,8 @@ ALLOWED_ATTRS = { # would make sense if we did "img": ["src"], } -ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] +# When bleach release a version with this option, we can specify schemes +#ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] class Mailer(object): @@ -184,7 +185,15 @@ class Mailer(object): defer.returnValue(ret) def get_message_vars(self, notif, event, room_state): - msgtype = event.content["msgtype"] + if event.type != "m.room.message": + return None + if event.content["msgtype"] != "m.text": + return None + + if "format" in event.content: + msgformat = event.content["format"] + else: + msgformat = None sender_state_event = room_state[("m.room.member", event.sender)] sender_name = name_from_member_event(sender_state_event) @@ -195,7 +204,7 @@ class Mailer(object): sender_hash = string_ordinal_total(event.sender) ret = { - "msgtype": msgtype, + "format": msgformat, "is_historical": event.event_id != notif['event_id'], "ts": event.origin_server_ts, "sender_name": sender_name, @@ -203,10 +212,10 @@ class Mailer(object): "sender_hash": sender_hash, } - if msgtype == "m.text": - ret["body_text_plain"] = event.content["body"] - elif msgtype == "org.matrix.custom.html": + if msgformat == "org.matrix.custom.html": ret["body_text_html"] = safe_markup(event.content["formatted_body"]) + else: + ret["body_text_plain"] = event.content["body"] return ret @@ -263,14 +272,14 @@ class Mailer(object): def make_unsubscribe_link(self): return "https://vector.im/#/settings" # XXX: matrix.to - def mxc_to_http_filter(self, value, width, height, resizeMethod="crop"): + def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": return "" serverAndMediaId = value[6:] params = { "width": width, "height": height, - "method": resizeMethod, + "method": resize_method, } return "%s_matrix/media/v1/thumbnail/%s?%s" % ( self.hs.config.public_baseurl, @@ -279,10 +288,12 @@ class Mailer(object): ) -def safe_markup(self, raw_html): +def safe_markup(raw_html): return jinja2.Markup(bleach.linkify(bleach.clean( raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, - protocols=ALLOWED_SCHEMES, strip=True + # bleach master has this, but it isn't released yet + # protocols=ALLOWED_SCHEMES, + strip=True ))) -- cgit 1.5.1 From 424a7f48f8d5cdb97ec3567d6841cecbf65ffda2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 17:50:49 +0100 Subject: Run filter_events_for_client so we don't accidentally mail out events people shouldn't see --- synapse/handlers/_base.py | 2 +- synapse/handlers/message.py | 8 ++++---- synapse/handlers/room.py | 2 +- synapse/handlers/search.py | 8 ++++---- synapse/handlers/sync.py | 4 ++-- synapse/notifier.py | 2 +- synapse/push/mailer.py | 18 +++++++++++------- 7 files changed, 24 insertions(+), 20 deletions(-) (limited to 'synapse/push') diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 13a675b208..134729069a 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -192,7 +192,7 @@ class BaseHandler(object): }) @defer.inlineCallbacks - def _filter_events_for_client(self, user_id, events, is_peeking=False): + def filter_events_for_client(self, user_id, events, is_peeking=False): """ Check which events a user is allowed to see diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index f51feda2f4..7d9e3cf364 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -123,7 +123,7 @@ class MessageHandler(BaseHandler): "end": next_token.to_string(), }) - events = yield self._filter_events_for_client( + events = yield self.filter_events_for_client( user_id, events, is_peeking=(member_event_id is None), @@ -483,7 +483,7 @@ class MessageHandler(BaseHandler): ] ).addErrback(unwrapFirstError) - messages = yield self._filter_events_for_client( + messages = yield self.filter_events_for_client( user_id, messages ) @@ -619,7 +619,7 @@ class MessageHandler(BaseHandler): end_token=stream_token ) - messages = yield self._filter_events_for_client( + messages = yield self.filter_events_for_client( user_id, messages, is_peeking=is_peeking ) @@ -700,7 +700,7 @@ class MessageHandler(BaseHandler): consumeErrors=True, ).addErrback(unwrapFirstError) - messages = yield self._filter_events_for_client( + messages = yield self.filter_events_for_client( user_id, messages, is_peeking=is_peeking, ) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index dd9c18df84..fdebc9c438 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -449,7 +449,7 @@ class RoomContextHandler(BaseHandler): now_token = yield self.hs.get_event_sources().get_current_token() def filter_evts(events): - return self._filter_events_for_client( + return self.filter_events_for_client( user.to_string(), events, is_peeking=is_guest) diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index 9937d8dd7f..a937e87408 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -172,7 +172,7 @@ class SearchHandler(BaseHandler): filtered_events = search_filter.filter([r["event"] for r in results]) - events = yield self._filter_events_for_client( + events = yield self.filter_events_for_client( user.to_string(), filtered_events ) @@ -223,7 +223,7 @@ class SearchHandler(BaseHandler): r["event"] for r in results ]) - events = yield self._filter_events_for_client( + events = yield self.filter_events_for_client( user.to_string(), filtered_events ) @@ -281,11 +281,11 @@ class SearchHandler(BaseHandler): event.room_id, event.event_id, before_limit, after_limit ) - res["events_before"] = yield self._filter_events_for_client( + res["events_before"] = yield self.filter_events_for_client( user.to_string(), res["events_before"] ) - res["events_after"] = yield self._filter_events_for_client( + res["events_after"] = yield self.filter_events_for_client( user.to_string(), res["events_after"] ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 231140b655..b51bb651ec 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -681,7 +681,7 @@ class SyncHandler(BaseHandler): if recents is not None: recents = sync_config.filter_collection.filter_room_timeline(recents) - recents = yield self._filter_events_for_client( + recents = yield self.filter_events_for_client( sync_config.user.to_string(), recents, ) @@ -702,7 +702,7 @@ class SyncHandler(BaseHandler): loaded_recents = sync_config.filter_collection.filter_room_timeline( events ) - loaded_recents = yield self._filter_events_for_client( + loaded_recents = yield self.filter_events_for_client( sync_config.user.to_string(), loaded_recents, ) diff --git a/synapse/notifier.py b/synapse/notifier.py index 6af7a8f424..cb58dfffd4 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -399,7 +399,7 @@ class Notifier(object): if name == "room": room_member_handler = self.hs.get_handlers().room_member_handler - new_events = yield room_member_handler._filter_events_for_client( + new_events = yield room_member_handler.filter_events_for_client( user.to_string(), new_events, is_peeking=is_peeking, diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index a4a0891e05..afdf439664 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -155,13 +155,15 @@ class Mailer(object): } for n in notifs: - vars = yield self.get_notif_vars(n, notif_events[n['event_id']], room_state) + vars = yield self.get_notif_vars( + n, user_id, notif_events[n['event_id']], room_state + ) room_vars['notifs'].append(vars) defer.returnValue(room_vars) @defer.inlineCallbacks - def get_notif_vars(self, notif, notif_event, room_state): + def get_notif_vars(self, notif, user_id, notif_event, room_state): results = yield self.store.get_events_around( notif['room_id'], notif['event_id'], before_limit=CONTEXT_BEFORE, after_limit=0 @@ -173,15 +175,17 @@ class Mailer(object): "messages": [], } - for event in results['events_before']: + handler = self.hs.get_handlers().message_handler + the_events = yield handler.filter_events_for_client( + user_id, results["events_before"] + ) + the_events.append(notif_event) + + for event in the_events: vars = self.get_message_vars(notif, event, room_state) if vars is not None: ret['messages'].append(vars) - vars = self.get_message_vars(notif, notif_event, room_state) - if vars is not None: - ret['messages'].append(vars) - defer.returnValue(ret) def get_message_vars(self, notif, event, room_state): -- cgit 1.5.1 From 9dba1b668ce1d2ea4db1fb6bfae3df319e7c76d4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 10:55:08 +0100 Subject: Linkify plain text messages too --- synapse/push/mailer.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index afdf439664..7ef64f8f6d 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -219,7 +219,7 @@ class Mailer(object): if msgformat == "org.matrix.custom.html": ret["body_text_html"] = safe_markup(event.content["formatted_body"]) else: - ret["body_text_plain"] = event.content["body"] + ret["body_text_html"] = safe_text(event.content["body"]) return ret @@ -301,6 +301,17 @@ def safe_markup(raw_html): ))) +def safe_text(raw_text): + """ + Process text: treat it as HTML but escape any tags (ie. just escape the + HTML) then linkify it. + """ + return jinja2.Markup(bleach.linkify(bleach.clean( + raw_text, tags=[], attributes={}, + strip=False + ))) + + def deduped_ordered_list(l): seen = set() ret = [] -- cgit 1.5.1 From ebbabc4986371c83d1d2659d10b27caad9b47951 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 11:49:36 +0100 Subject: Handle room invites in email notifs --- res/templates/room.html | 14 +++++++++----- synapse/push/mailer.py | 35 ++++++++++++++++++++++++++++++----- synapse/util/presentable_names.py | 6 +++--- 3 files changed, 42 insertions(+), 13 deletions(-) (limited to 'synapse/push') diff --git a/res/templates/room.html b/res/templates/room.html index f369575b98..6c68ee1fdc 100644 --- a/res/templates/room.html +++ b/res/templates/room.html @@ -13,9 +13,13 @@ {% endif %} {% endif %}
-
- {% for notif in room.notifs %} - {% include 'notif.html' with context %} - {% endfor %} -
+ {% if room.invite %} + Join the conversation. + {% else %} +
+ {% for notif in room.notifs %} + {% include 'notif.html' with context %} + {% endfor %} +
+ {% endif %} diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 7ef64f8f6d..d2cf24765a 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -39,6 +39,8 @@ MESSAGE_FROM_PERSON = "You have a message from %s" MESSAGES_FROM_PERSON = "You have messages from %s" MESSAGES_IN_ROOM = "There are some messages for you in the %s room" MESSAGES_IN_ROOMS = "Here are some messages you may have missed" +INVITE_FROM_PERSON_TO_ROOM = "%s has invited you to join the %s room" +INVITE_FROM_PERSON = "%s has invited you to chat" CONTEXT_BEFORE = 1 @@ -148,17 +150,24 @@ class Mailer(object): @defer.inlineCallbacks def get_room_vars(self, room_id, user_id, notifs, notif_events, room_state): + my_member_event = room_state[("m.room.member", user_id)] + is_invite = my_member_event.content["membership"] == "invite" + room_vars = { "title": calculate_room_name(room_state, user_id), "hash": string_ordinal_total(room_id), # See sender avatar hash "notifs": [], + "invite": is_invite } - for n in notifs: - vars = yield self.get_notif_vars( - n, user_id, notif_events[n['event_id']], room_state - ) - room_vars['notifs'].append(vars) + if is_invite: + room_vars["link"] = self.make_room_link(room_id) + else: + for n in notifs: + vars = yield self.get_notif_vars( + n, user_id, notif_events[n['event_id']], room_state + ) + room_vars['notifs'].append(vars) defer.returnValue(room_vars) @@ -235,6 +244,18 @@ class Mailer(object): state_by_room[room_id], user_id, fallback_to_members=False ) + my_member_event = state_by_room[room_id][("m.room.member", user_id)] + if my_member_event.content["membership"] == "invite": + inviter_member_event = state_by_room[room_id][ + ("m.room.member", my_member_event.sender) + ] + inviter_name = name_from_member_event(inviter_member_event) + + if room_name is None: + return INVITE_FROM_PERSON % (inviter_name,) + else: + return INVITE_FROM_PERSON_TO_ROOM % (inviter_name, room_name) + sender_name = None if len(notifs_by_room[room_id]) == 1: # There is just the one notification, so give some detail @@ -242,6 +263,7 @@ class Mailer(object): if ("m.room.member", event.sender) in state_by_room[room_id]: state_event = state_by_room[room_id][("m.room.member", event.sender)] sender_name = name_from_member_event(state_event) + if sender_name is not None and room_name is not None: return MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) elif sender_name is not None: @@ -268,6 +290,9 @@ class Mailer(object): # Stuff's happened in multiple different rooms return MESSAGES_IN_ROOMS + def make_room_link(self, room_id): + return "https://matrix.to/%s" % (room_id,) + def make_notif_link(self, notif): return "https://matrix.to/%s/%s" % ( notif['room_id'], notif['event_id'] diff --git a/synapse/util/presentable_names.py b/synapse/util/presentable_names.py index 2ae01e453d..f80a7fe58e 100644 --- a/synapse/util/presentable_names.py +++ b/synapse/util/presentable_names.py @@ -52,6 +52,9 @@ def calculate_room_name(room_state, user_id, fallback_to_members=True): if len(the_aliases) > 0 and _looks_like_an_alias(the_aliases[0]): return the_aliases[0] + if not fallback_to_members: + return None + my_member_event = None if ("m.room.member", user_id) in room_state: my_member_event = room_state[("m.room.member", user_id)] @@ -66,9 +69,6 @@ def calculate_room_name(room_state, user_id, fallback_to_members=True): else: return "Room Invite" - if not fallback_to_members: - return None - # we're going to have to generate a name based on who's in the room, # so find out who is in the room that isn't the user. if "m.room.member" in room_state_bytype: -- cgit 1.5.1 From 937c407eef78648f1c4c1afe56fdfd8598315b19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 15:12:14 +0100 Subject: Only import email pusher if email notifs are on --- synapse/push/pusher.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index f7c3021fcc..25a45af775 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,12 +1,14 @@ from httppusher import HttpPusher -from emailpusher import EmailPusher - -PUSHER_TYPES = { - 'http': HttpPusher, - 'email': EmailPusher, -} def create_pusher(hs, pusherdict): + PUSHER_TYPES = { + "http": HttpPusher, + } + + if hs.config.email_enable_notifs: + from emailpusher import EmailPusher + PUSHER_TYPES["email"] = EmailPusher + if pusherdict['kind'] in PUSHER_TYPES: return PUSHER_TYPES[pusherdict['kind']](hs, pusherdict) -- cgit 1.5.1 From 60f86fc876bcb106842d804baac9aff4860ece3b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 15:16:30 +0100 Subject: pep8 --- synapse/config/emailconfig.py | 2 +- synapse/push/mailer.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'synapse/push') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 893034e2ef..06b076e3f9 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -48,7 +48,7 @@ class EmailConfig(Config): if (len(missing) > 0): raise RuntimeError( "email.enable_notifs is True but required keys are missing: %s" % - (", ".join(["email."+k for k in missing]),) + (", ".join(["email." + k for k in missing]),) ) if config.get("public_baseurl") is None: diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index d2cf24765a..ae3e41b8ce 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -62,7 +62,7 @@ ALLOWED_ATTRS = { "img": ["src"], } # When bleach release a version with this option, we can specify schemes -#ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] +# ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] class Mailer(object): @@ -283,7 +283,8 @@ class Mailer(object): return MESSAGES_FROM_PERSON % ( descriptor_from_member_events([ - state_by_room[room_id][("m.room.member", s)] for s in sender_ids + state_by_room[room_id][("m.room.member", s)] + for s in sender_ids ]) ) else: @@ -346,11 +347,13 @@ def deduped_ordered_list(l): ret.append(item) return ret + def string_ordinal_total(s): tot = 0 for c in s: tot += ord(c) return tot + def format_ts_filter(value, format): return time.strftime(format, time.localtime(value / 1000)) -- cgit 1.5.1 From 4845c7359de6c1ad1e7132653939e44ee8ff9156 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 15:55:53 +0100 Subject: Support image notifs --- res/templates/notif.html | 12 ++++++++---- synapse/push/mailer.py | 36 +++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 15 deletions(-) (limited to 'synapse/push') diff --git a/res/templates/notif.html b/res/templates/notif.html index aa6ed1e061..bdff2786ff 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -17,10 +17,14 @@
{{ message.sender_name }}
{{ message.ts|format_ts("%H:%M") }}
- {% if message.format == "org.matrix.custom.html" %} - {{ message.body_text_html }} - {% else %} - {{ message.body_text_plain }} + {% if message.msgtype == "m.text" %} + {% if message.format == "org.matrix.custom.html" %} + {{ message.body_text_html }} + {% else %} + {{ message.body_text_plain }} + {% endif %} + {% elif message.msgtype == "m.image" %} + {% endif %}
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index ae3e41b8ce..e6554dc7fd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -200,13 +200,6 @@ class Mailer(object): def get_message_vars(self, notif, event, room_state): if event.type != "m.room.message": return None - if event.content["msgtype"] != "m.text": - return None - - if "format" in event.content: - msgformat = event.content["format"] - else: - msgformat = None sender_state_event = room_state[("m.room.member", event.sender)] sender_name = name_from_member_event(sender_state_event) @@ -217,7 +210,7 @@ class Mailer(object): sender_hash = string_ordinal_total(event.sender) ret = { - "format": msgformat, + "msgtype": event.content["msgtype"], "is_historical": event.event_id != notif['event_id'], "ts": event.origin_server_ts, "sender_name": sender_name, @@ -225,13 +218,34 @@ class Mailer(object): "sender_hash": sender_hash, } - if msgformat == "org.matrix.custom.html": - ret["body_text_html"] = safe_markup(event.content["formatted_body"]) + if event.content["msgtype"] == "m.text": + self.add_text_message_vars(ret, event) + elif event.content["msgtype"] == "m.image": + self.add_image_message_vars(ret, event) else: - ret["body_text_html"] = safe_text(event.content["body"]) + return None return ret + def add_text_message_vars(self, vars, event): + if "format" in event.content: + msgformat = event.content["format"] + else: + msgformat = None + vars["format"] = msgformat + + if msgformat == "org.matrix.custom.html": + vars["body_text_html"] = safe_markup(event.content["formatted_body"]) + else: + vars["body_text_html"] = safe_text(event.content["body"]) + + return vars + + def add_image_message_vars(self, vars, event): + vars["image_url"] = event.content["url"] + + return vars + def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: # Only one room has new stuff -- cgit 1.5.1 From 68f8fc2f143d44b560b07e7521b28aae332e243d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 16:59:57 +0100 Subject: Support file messages & fix plain text --- res/templates/notif.html | 8 +++----- synapse/push/mailer.py | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'synapse/push') diff --git a/res/templates/notif.html b/res/templates/notif.html index bdff2786ff..70f5655352 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -18,13 +18,11 @@
{{ message.ts|format_ts("%H:%M") }}
{% if message.msgtype == "m.text" %} - {% if message.format == "org.matrix.custom.html" %} - {{ message.body_text_html }} - {% else %} - {{ message.body_text_plain }} - {% endif %} + {{ message.body_text_html }} {% elif message.msgtype == "m.image" %} + {% elif message.msgtype == "m.file" %} + {{ message.body_text_plain }} {% endif %}
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e6554dc7fd..60a4878a3e 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -222,8 +222,9 @@ class Mailer(object): self.add_text_message_vars(ret, event) elif event.content["msgtype"] == "m.image": self.add_image_message_vars(ret, event) - else: - return None + + if "body" in event.content: + ret["body_text_plain"] = event.content["body"] return ret -- cgit 1.5.1 From cc0874cf71b2d2eef713155900b4ac5384ba1a1a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 17:00:40 +0100 Subject: Put back real delay before mailing --- synapse/push/emailpusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 6ae16e9865..dcbee4c3fe 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification # (to give the user a chance to respond to other push or notice the window) -DELAY_BEFORE_MAIL_MS = 2000#2 * 60 * 1000 +DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days -- cgit 1.5.1 From e800ee2f636a6aa63a9ac35388b9f982b01cd1c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 17:28:27 +0100 Subject: May as well always include room link --- synapse/push/mailer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 60a4878a3e..c53ae9a547 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -157,12 +157,11 @@ class Mailer(object): "title": calculate_room_name(room_state, user_id), "hash": string_ordinal_total(room_id), # See sender avatar hash "notifs": [], - "invite": is_invite + "invite": is_invite, + "link": self.make_room_link(room_id), } - if is_invite: - room_vars["link"] = self.make_room_link(room_id) - else: + if not is_invite: for n in notifs: vars = yield self.get_notif_vars( n, user_id, notif_events[n['event_id']], room_state -- cgit 1.5.1 From b2c04da8dc98ca09620dc207c95f68b2e8a52e62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 11:43:57 +0100 Subject: Add an email pusher for new users If they registered with an email address and email notifs are enabled on the HS --- synapse/push/pusherpool.py | 1 + synapse/rest/client/v2_alpha/register.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 6ef48d63f7..7fef2fb6f7 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -50,6 +50,7 @@ class PusherPool: # recreated, added and started: this means we have only one # code path adding pushers. pusher.create_pusher(self.hs, { + "id": None, "user_name": user_id, "kind": kind, "app_id": app_id, diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ff8f69ddbf..883b1c1291 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -48,6 +48,7 @@ class RegisterRestServlet(RestServlet): super(RegisterRestServlet, self).__init__() self.hs = hs self.auth = hs.get_auth() + self.store = hs.get_datastore() self.auth_handler = hs.get_handlers().auth_handler self.registration_handler = hs.get_handlers().registration_handler self.identity_handler = hs.get_handlers().identity_handler @@ -214,6 +215,31 @@ class RegisterRestServlet(RestServlet): threepid['validated_at'], ) + # And we add an email pusher for them by default, but only + # if email notifications are enabled (so people don't start + # getting mail spam where they weren't before if email + # notifs are set up on a home server) + if self.hs.config.email_enable_notifs: + # Pull the ID of the access token back out of the db + # It would really make more sense for this to be passed + # up when the access token is saved, but that's quite an + # invasive change I'd rather do separately. + user_tuple = yield self.store.get_user_by_access_token( + token + ) + + yield self.hs.get_pusherpool().add_pusher( + user_id=user_id, + access_token=user_tuple["token_id"], + kind="email", + app_id="m.email", + app_display_name="Email Notifications", + device_display_name=threepid["address"], + pushkey=threepid["address"], + lang=None, # We don't know a user's language here + data={}, + ) + if 'bind_email' in params and params['bind_email']: logger.info("bind_email specified: binding") -- cgit 1.5.1 From 40d40e470d9ff047f4255a93e40af8a9870b43cc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 13:56:21 +0100 Subject: Send mail notifs with a plaintext part too --- res/templates/notif.txt | 12 ++++++++++++ res/templates/notif_mail.txt | 10 ++++++++++ res/templates/room.txt | 6 ++++++ synapse/config/emailconfig.py | 2 ++ synapse/push/mailer.py | 27 ++++++++++++++++++++------- 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 res/templates/notif.txt create mode 100644 res/templates/notif_mail.txt create mode 100644 res/templates/room.txt (limited to 'synapse/push') diff --git a/res/templates/notif.txt b/res/templates/notif.txt new file mode 100644 index 0000000000..b515f394c3 --- /dev/null +++ b/res/templates/notif.txt @@ -0,0 +1,12 @@ +{% for message in notif.messages %} +{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }}) +{% if message.msgtype == "m.text" %} +{{ message.body_text_plain }} +{% elif message.msgtype == "m.image" %} +{{ message.body_text_plain }} +{% elif message.msgtype == "m.file" %} +{{ message.body_text_plain }} +{% endif %} +{% endfor %} + +View at {{ notif.link }} diff --git a/res/templates/notif_mail.txt b/res/templates/notif_mail.txt new file mode 100644 index 0000000000..24843042a5 --- /dev/null +++ b/res/templates/notif_mail.txt @@ -0,0 +1,10 @@ +Hi {{ user_display_name }}, + +{{ summary_text }} + +{% for room in rooms %} +{% include 'room.txt' with context %} +{% endfor %} + +You can disable these notifications at {{ unsubscribe_link }} + diff --git a/res/templates/room.txt b/res/templates/room.txt new file mode 100644 index 0000000000..999d0ae60a --- /dev/null +++ b/res/templates/room.txt @@ -0,0 +1,6 @@ +{{ room.title }} +You've been invited, join at {{ room.link }} + +{% for notif in room.notifs %} +{% include 'notif.txt' with context %} +{% endfor %} diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 06b076e3f9..b7be67f173 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -38,6 +38,7 @@ class EmailConfig(Config): "notif_from", "template_dir", "notif_template_html", + "notif_template_text", ] missing = [] @@ -61,6 +62,7 @@ class EmailConfig(Config): self.email_notif_from = email_config["notif_from"] self.email_template_dir = email_config["template_dir"] self.email_notif_template_html = email_config["notif_template_html"] + self.email_notif_template_text = email_config["notif_template_text"] # make sure it's valid parsed = email.utils.parseaddr(self.email_notif_from) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index c53ae9a547..4fd89b3e90 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -19,6 +19,7 @@ from twisted.mail.smtp import sendmail import email.utils import email.mime.multipart from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart from synapse.util.async import concurrently_execute from synapse.util.presentable_names import ( @@ -74,7 +75,12 @@ class Mailer(object): env = jinja2.Environment(loader=loader) env.filters["format_ts"] = format_ts_filter env.filters["mxc_to_http"] = self.mxc_to_http_filter - self.notif_template = env.get_template(self.hs.config.email_notif_template_html) + self.notif_template_html = env.get_template( + self.hs.config.email_notif_template_html + ) + self.notif_template_text = env.get_template( + self.hs.config.email_notif_template_text + ) @defer.inlineCallbacks def send_notification_mail(self, user_id, email_address, push_actions): @@ -135,16 +141,23 @@ class Mailer(object): "rooms": rooms, } - plainText = self.notif_template.render(**template_vars) + html_text = self.notif_template_html.render(**template_vars) + html_part = MIMEText(html_text, "html", "utf8") + + plain_text = self.notif_template_text.render(**template_vars) + text_part = MIMEText(plain_text, "plain", "utf8") + + multipart_msg = MIMEMultipart('alternative') + multipart_msg['Subject'] = "New Matrix Notifications" + multipart_msg['From'] = self.hs.config.email_notif_from + multipart_msg['To'] = email_address + multipart_msg.attach(text_part) + multipart_msg.attach(html_part) - text_part = MIMEText(plainText, "html", "utf8") - text_part['Subject'] = "New Matrix Notifications" - text_part['From'] = self.hs.config.email_notif_from - text_part['To'] = email_address yield sendmail( self.hs.config.email_smtp_host, - raw_from, raw_to, text_part.as_string(), + raw_from, raw_to, multipart_msg.as_string(), port=self.hs.config.email_smtp_port ) -- cgit 1.5.1 From 4364ea1272a2201fc434f4cf6f0c3d13f5cbc37e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:31:27 +0100 Subject: Stop processing notifs once we've sent a mail --- synapse/push/emailpusher.py | 1 + 1 file changed, 1 insertion(+) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index dcbee4c3fe..68f59a3faa 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -159,6 +159,7 @@ class EmailPusher(object): yield self.sent_notif_update_throttle( push_action['room_id'], push_action ) + break else: if soonest_due_at is None or should_notify_at < soonest_due_at: soonest_due_at = should_notify_at -- cgit 1.5.1 From 3facde2536f392477d59c2e03be2f2fb07e383dc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:36:45 +0100 Subject: Remove rather pointless get function --- synapse/push/emailpusher.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 68f59a3faa..62e1e40326 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -139,9 +139,7 @@ class EmailPusher(object): notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS room_ready_at = self.room_ready_to_notify_at( - push_action['room_id'], self.get_room_last_notif_ts( - last_notifs, push_action['room_id'] - ) + push_action['room_id'], last_notifs.get(push_action['room_id'], 0) ) should_notify_at = max(notif_ready_at, room_ready_at) @@ -184,12 +182,6 @@ class EmailPusher(object): def seconds_until(self, ts_msec): return (ts_msec - self.clock.time_msec()) / 1000 - def get_room_last_notif_ts(self, last_notif_by_room, room_id): - if room_id in last_notif_by_room: - return last_notif_by_room[room_id] - else: - return 0 - def get_room_throttle_ms(self, room_id): if room_id in self.throttle_params: return self.throttle_params[room_id]["throttle_ms"] -- cgit 1.5.1 From 311b5ce051cf979f30f6aa53051b1349326b2235 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:37:30 +0100 Subject: pep8 --- synapse/push/mailer.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 4fd89b3e90..0ab5168e20 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -154,7 +154,6 @@ class Mailer(object): multipart_msg.attach(text_part) multipart_msg.attach(html_part) - yield sendmail( self.hs.config.email_smtp_host, raw_from, raw_to, multipart_msg.as_string(), -- cgit 1.5.1 From d3da5294e8acaba34c59bdfc62f7010363b17a18 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:04:40 +0100 Subject: Use named parameter format --- synapse/push/mailer.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 0ab5168e20..b193bb84b1 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -35,13 +35,13 @@ import time import urllib -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" -MESSAGE_FROM_PERSON = "You have a message from %s" -MESSAGES_FROM_PERSON = "You have messages from %s" -MESSAGES_IN_ROOM = "There are some messages for you in the %s room" +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %(person)s in the %s room" +MESSAGE_FROM_PERSON = "You have a message from %(person)s" +MESSAGES_FROM_PERSON = "You have messages from %(person)s" +MESSAGES_IN_ROOM = "There are some messages for you in the %(room)s room" MESSAGES_IN_ROOMS = "Here are some messages you may have missed" -INVITE_FROM_PERSON_TO_ROOM = "%s has invited you to join the %s room" -INVITE_FROM_PERSON = "%s has invited you to chat" +INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room" +INVITE_FROM_PERSON = "%(person)s has invited you to chat" CONTEXT_BEFORE = 1 @@ -278,9 +278,11 @@ class Mailer(object): inviter_name = name_from_member_event(inviter_member_event) if room_name is None: - return INVITE_FROM_PERSON % (inviter_name,) + return INVITE_FROM_PERSON % {"person": inviter_name} else: - return INVITE_FROM_PERSON_TO_ROOM % (inviter_name, room_name) + return INVITE_FROM_PERSON_TO_ROOM % { + "person": inviter_name, "room": room_name + } sender_name = None if len(notifs_by_room[room_id]) == 1: @@ -291,14 +293,20 @@ class Mailer(object): sender_name = name_from_member_event(state_event) if sender_name is not None and room_name is not None: - return MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) + return MESSAGE_FROM_PERSON_IN_ROOM % { + "person": sender_name, "room": room_name + } elif sender_name is not None: - return MESSAGE_FROM_PERSON % (sender_name,) + return MESSAGE_FROM_PERSON % { + "person": sender_name + } else: # There's more than one notification for this room, so just # say there are several if room_name is not None: - return MESSAGES_IN_ROOM % (room_name,) + return MESSAGES_IN_ROOM % { + "room": room_name + } else: # If the room doesn't have a name, say who the messages # are from explicitly to avoid, "messages in the Bob room" @@ -307,12 +315,12 @@ class Mailer(object): for n in notifs_by_room[room_id] ])) - return MESSAGES_FROM_PERSON % ( - descriptor_from_member_events([ + return MESSAGES_FROM_PERSON % { + "person": descriptor_from_member_events([ state_by_room[room_id][("m.room.member", s)] for s in sender_ids ]) - ) + } else: # Stuff's happened in multiple different rooms return MESSAGES_IN_ROOMS -- cgit 1.5.1 From 29c8cf8db81b89b1571b0a042d2bfed3ed278890 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:09:28 +0100 Subject: Avoid `vars` builtin --- synapse/push/mailer.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index b193bb84b1..95f872ccf0 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -125,10 +125,10 @@ class Mailer(object): rooms = [] for r in rooms_in_order: - vars = yield self.get_room_vars( + roomvars = yield self.get_room_vars( r, user_id, notifs_by_room[r], notif_events, state_by_room[r] ) - rooms.append(vars) + rooms.append(roomvars) summary_text = self.make_summary_text( notifs_by_room, state_by_room, notif_events, user_id @@ -175,10 +175,10 @@ class Mailer(object): if not is_invite: for n in notifs: - vars = yield self.get_notif_vars( + notifvars = yield self.get_notif_vars( n, user_id, notif_events[n['event_id']], room_state ) - room_vars['notifs'].append(vars) + room_vars['notifs'].append(notifvars) defer.returnValue(room_vars) @@ -202,9 +202,9 @@ class Mailer(object): the_events.append(notif_event) for event in the_events: - vars = self.get_message_vars(notif, event, room_state) - if vars is not None: - ret['messages'].append(vars) + messagevars = self.get_message_vars(notif, event, room_state) + if messagevars is not None: + ret['messages'].append(messagevars) defer.returnValue(ret) @@ -239,24 +239,24 @@ class Mailer(object): return ret - def add_text_message_vars(self, vars, event): + def add_text_message_vars(self, messagevars, event): if "format" in event.content: msgformat = event.content["format"] else: msgformat = None - vars["format"] = msgformat + messagevars["format"] = msgformat if msgformat == "org.matrix.custom.html": - vars["body_text_html"] = safe_markup(event.content["formatted_body"]) + messagevars["body_text_html"] = safe_markup(event.content["formatted_body"]) else: - vars["body_text_html"] = safe_text(event.content["body"]) + messagevars["body_text_html"] = safe_text(event.content["body"]) - return vars + return messagevars - def add_image_message_vars(self, vars, event): - vars["image_url"] = event.content["url"] + def add_image_message_vars(self, messagevars, event): + messagevars["image_url"] = event.content["url"] - return vars + return messagevars def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: -- cgit 1.5.1 From e7a76b512302ee77e3afc6c17dfb2efc3b58b156 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:10:45 +0100 Subject: Use the constant --- synapse/push/mailer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 95f872ccf0..1e6654e2bb 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -27,6 +27,7 @@ from synapse.util.presentable_names import ( ) from synapse.types import UserID from synapse.api.errors import StoreError +from synapse.api.constants import EventTypes import jinja2 import bleach @@ -209,7 +210,7 @@ class Mailer(object): defer.returnValue(ret) def get_message_vars(self, notif, event, room_state): - if event.type != "m.room.message": + if event.type != EventTypes.Message: return None sender_state_event = room_state[("m.room.member", event.sender)] -- cgit 1.5.1 From 6b9b6a91691d543fe420e82366f641beb760cf11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 20:02:52 +0100 Subject: Remove unused arg --- synapse/push/emailpusher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 62e1e40326..8b105b85c8 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -139,7 +139,7 @@ class EmailPusher(object): notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS room_ready_at = self.room_ready_to_notify_at( - push_action['room_id'], last_notifs.get(push_action['room_id'], 0) + push_action['room_id'] ) should_notify_at = max(notif_ready_at, room_ready_at) @@ -194,7 +194,7 @@ class EmailPusher(object): else: return 0 - def room_ready_to_notify_at(self, room_id, last_notif_time): + def room_ready_to_notify_at(self, room_id): """ Determines whether throttling should prevent us from sending an email for the given room -- cgit 1.5.1 From 35b7b8e4bccd8cfbe44b2ffee97cdee0a48701ba Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 20:10:34 +0100 Subject: Remove unused function --- synapse/push/emailpusher.py | 4 ---- synapse/storage/event_push_actions.py | 22 ---------------------- 2 files changed, 26 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 8b105b85c8..c10deded06 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -122,10 +122,6 @@ class EmailPusher(object): up logging, measures and guards against multiple instances of it being run. """ - last_notifs = yield self.store.get_time_of_latest_push_action_by_room_for_user( - self.user_id - ) - unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( self.user_id, self.last_stream_ordering, self.max_stream_ordering ) diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index b710101b15..85290d2a90 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -212,28 +212,6 @@ class EventPushActionsStore(SQLBaseStore): ) defer.returnValue(result[0] or 0) - @defer.inlineCallbacks - def get_time_of_latest_push_action_by_room_for_user(self, user_id): - """ - Returns only the received_ts of the last notification in each of the - user's rooms, in a dict by room_id - """ - def f(txn): - txn.execute( - "SELECT ep.room_id, MAX(e.received_ts)" - " FROM event_push_actions AS ep" - " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" - " WHERE ep.user_id = ?" - " GROUP BY ep.room_id", - (user_id,) - ) - return txn.fetchall() - result = yield self.runInteraction( - "get_time_of_latest_push_action_by_room_for_user", f - ) - - defer.returnValue({row[0]: row[1] for row in result}) - def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): # Sad that we have to blow away the cache for the whole room here txn.call_after( -- cgit 1.5.1 From b084e4d963f2df377dfe98b6d9645fa9183a368b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 20:14:55 +0100 Subject: Add constant for throttle multiplier --- synapse/push/emailpusher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index c10deded06..3a13c7485a 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -30,6 +30,7 @@ DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days +THROTTLE_MULTIPLIER = 2 # If no event triggers a notification for this long after the previous, # the throttle is released. @@ -231,7 +232,7 @@ class EmailPusher(object): new_throttle_ms = THROTTLE_START_MS else: new_throttle_ms = min( - current_throttle_ms * 2, + current_throttle_ms * THROTTLE_MULTIPLIER, THROTTLE_MAX_MS ) self.throttle_params[room_id] = { -- cgit 1.5.1 From 92f0f3d21d52ae14a3d4d4536a84055b92d228ae Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:24:07 +0100 Subject: Catch all exceptions when creating a pusher --- synapse/push/pusherpool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 7fef2fb6f7..66eafb69d8 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -186,8 +186,8 @@ class PusherPool: for pusherdict in pushers: try: p = pusher.create_pusher(self.hs, pusherdict) - except PusherConfigException: - logger.exception("Couldn't start a pusher: caught PusherConfigException") + except: + logger.exception("Couldn't start a pusher: caught Exception") continue if p: appid_pushkey = "%s:%s" % ( -- cgit 1.5.1 From e6bffa4475e1a37643317d709b2003cdf99c149f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:26:58 +0100 Subject: Unused import --- synapse/push/pusherpool.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 66eafb69d8..5853ec36a9 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -17,7 +17,6 @@ from twisted.internet import defer import pusher -from synapse.push import PusherConfigException from synapse.util.logcontext import preserve_fn from synapse.util.async import run_on_reactor -- cgit 1.5.1 From f1026418ea0f2dba6cc4075b737ac6fa4ffac89d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 4 May 2016 11:37:42 +0100 Subject: copyright --- synapse/push/pusher.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index 25a45af775..d1eaa236bd 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,14 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from httppusher import HttpPusher +import logging +logger = logging.getLogger(__name__) def create_pusher(hs, pusherdict): + logger.info("trying to create_pusher for %r", pusherdict) + PUSHER_TYPES = { "http": HttpPusher, } + logger.info("email enable notifs: %r", hs.config.email_enable_notifs) if hs.config.email_enable_notifs: from emailpusher import EmailPusher PUSHER_TYPES["email"] = EmailPusher + logger.info("defined email pusher type") if pusherdict['kind'] in PUSHER_TYPES: + logger.info("found pusher") return PUSHER_TYPES[pusherdict['kind']](hs, pusherdict) -- cgit 1.5.1 From de22001ab55d7fb89e4fd1527a4b419f5c195458 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:41:35 +0100 Subject: pep8 --- synapse/push/pusher.py | 1 + 1 file changed, 1 insertion(+) (limited to 'synapse/push') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index d1eaa236bd..db5c1f1aa6 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -18,6 +18,7 @@ from httppusher import HttpPusher import logging logger = logging.getLogger(__name__) + def create_pusher(hs, pusherdict): logger.info("trying to create_pusher for %r", pusherdict) -- cgit 1.5.1 From 9ef05a12c380a6a5a45226d6086d70512fd4f073 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 14:52:10 +0100 Subject: Add date header & message id --- synapse/app/pusher.py | 3 ++- synapse/push/mailer.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 9976908983..89c8d5c7ce 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -20,6 +20,7 @@ from synapse.server import HomeServer from synapse.config._base import ConfigError from synapse.config.database import DatabaseConfig from synapse.config.logger import LoggingConfig +from synapse.config.emailconfig import EmailConfig from synapse.http.site import SynapseSite from synapse.metrics.resource import MetricsResource, METRICS_PREFIX from synapse.replication.slave.storage.events import SlavedEventStore @@ -91,7 +92,7 @@ class SlaveConfig(DatabaseConfig): """ % locals() -class PusherSlaveConfig(SlaveConfig, LoggingConfig): +class PusherSlaveConfig(SlaveConfig, LoggingConfig, EmailConfig): pass diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 1e6654e2bb..037bc990d0 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -152,6 +152,8 @@ class Mailer(object): multipart_msg['Subject'] = "New Matrix Notifications" multipart_msg['From'] = self.hs.config.email_notif_from multipart_msg['To'] = email_address + multipart_msg['Date'] = email.utils.formatdate() + multipart_msg['Message-ID'] = email.utils.make_msgid() multipart_msg.attach(text_part) multipart_msg.attach(html_part) -- cgit 1.5.1 From 39d0a99972cc58e8d2c7f7997d13aa18d547a2d6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 14:52:49 +0100 Subject: Include no context until we can de-dup between the context and other notifs --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 037bc990d0..ad44ba985e 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -44,7 +44,7 @@ MESSAGES_IN_ROOMS = "Here are some messages you may have missed" INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room" INVITE_FROM_PERSON = "%(person)s has invited you to chat" -CONTEXT_BEFORE = 1 +CONTEXT_BEFORE = 0 # From https://github.com/matrix-org/matrix-react-sdk/blob/master/src/HtmlUtils.js ALLOWED_TAGS = [ -- cgit 1.5.1 From 1cf5c379cb101c89897750d79e4548c172581f48 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 01:54:12 +0100 Subject: spell out emailpusher full path --- synapse/push/pusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index db5c1f1aa6..e6c0806415 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -28,7 +28,7 @@ def create_pusher(hs, pusherdict): logger.info("email enable notifs: %r", hs.config.email_enable_notifs) if hs.config.email_enable_notifs: - from emailpusher import EmailPusher + from synapse.push.emailpusher import EmailPusher PUSHER_TYPES["email"] = EmailPusher logger.info("defined email pusher type") -- cgit 1.5.1 From ce81ccb063c46bf1bc3adca0d5dcfbc786624641 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 01:56:43 +0100 Subject: handle fragments correctly on mxc URLs. switch to vector.im permalinks as matrix.to isn't ready yet. merge overlapping notifications together. give one message of context after a notification (in the unlikely event it exists, but it's possible thanks to throttling). include name of app in mail templates --- synapse/push/mailer.py | 59 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 14 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index ad44ba985e..5951c14ce1 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -35,16 +35,20 @@ import bleach import time import urllib +import logging +logger = logging.getLogger(__name__) -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %(person)s in the %s room" -MESSAGE_FROM_PERSON = "You have a message from %(person)s" -MESSAGES_FROM_PERSON = "You have messages from %(person)s" -MESSAGES_IN_ROOM = "There are some messages for you in the %(room)s room" -MESSAGES_IN_ROOMS = "Here are some messages you may have missed" -INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room" -INVITE_FROM_PERSON = "%(person)s has invited you to chat" -CONTEXT_BEFORE = 0 +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% from %(person)s in the %s room..." +MESSAGE_FROM_PERSON = "You have a message on %%app%% from %(person)s..." +MESSAGES_FROM_PERSON = "You have messages on %%app%% from %(person)s..." +MESSAGES_IN_ROOM = "There are some messages on %%app%% for you in the %(room)s room..." +MESSAGES_IN_ROOMS = "Here are some messages on %%app%% you may have missed..." +INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room on %%app%%..." +INVITE_FROM_PERSON = "%(person)s has invited you to chat on %%app%%..." + +CONTEXT_BEFORE = 1 +CONTEXT_AFTER = 1 # From https://github.com/matrix-org/matrix-react-sdk/blob/master/src/HtmlUtils.js ALLOWED_TAGS = [ @@ -181,7 +185,25 @@ class Mailer(object): notifvars = yield self.get_notif_vars( n, user_id, notif_events[n['event_id']], room_state ) - room_vars['notifs'].append(notifvars) + + # merge overlapping notifs together. + # relies on the notifs being in chronological order. + merge = False + if room_vars['notifs'] and 'messages' in room_vars['notifs'][-1]: + prev_messages = room_vars['notifs'][-1]['messages'] + for message in notifvars['messages']: + pm = filter(lambda pm: pm['id'] == message['id'], prev_messages) + if pm: + if not message["is_historical"]: + pm[0]["is_historical"] = False + merge = True + elif merge: + # we're merging, so append any remaining messages + # in this notif to the previous one + prev_messages.append(message) + + if not merge: + room_vars['notifs'].append(notifvars) defer.returnValue(room_vars) @@ -189,7 +211,7 @@ class Mailer(object): def get_notif_vars(self, notif, user_id, notif_event, room_state): results = yield self.store.get_events_around( notif['room_id'], notif['event_id'], - before_limit=CONTEXT_BEFORE, after_limit=0 + before_limit=CONTEXT_BEFORE, after_limit=CONTEXT_AFTER ) ret = { @@ -226,6 +248,7 @@ class Mailer(object): ret = { "msgtype": event.content["msgtype"], "is_historical": event.event_id != notif['event_id'], + "id": event.event_id, "ts": event.origin_server_ts, "sender_name": sender_name, "sender_avatar_url": sender_avatar_url, @@ -329,10 +352,12 @@ class Mailer(object): return MESSAGES_IN_ROOMS def make_room_link(self, room_id): - return "https://matrix.to/%s" % (room_id,) + # XXX: matrix.to + return "https://vector.im/#/room/%s" % (room_id,) def make_notif_link(self, notif): - return "https://matrix.to/%s/%s" % ( + # XXX: matrix.to + return "https://vector.im/#/room/%s/%s" % ( notif['room_id'], notif['event_id'] ) @@ -342,16 +367,22 @@ class Mailer(object): def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": return "" + serverAndMediaId = value[6:] + if '#' in serverAndMediaId: + (serverAndMediaId, fragment) = serverAndMediaId.split('#', 1) + fragment = "#" + fragment + params = { "width": width, "height": height, "method": resize_method, } - return "%s_matrix/media/v1/thumbnail/%s?%s" % ( + return "%s_matrix/media/v1/thumbnail/%s?%s%s" % ( self.hs.config.public_baseurl, serverAndMediaId, - urllib.urlencode(params) + urllib.urlencode(params), + fragment or "", ) -- cgit 1.5.1 From 634efb65f1ca8d3485c75a828a98161cfd617bb6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 02:10:57 +0100 Subject: pep8 --- synapse/push/mailer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 5951c14ce1..aab6387125 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -39,12 +39,14 @@ import logging logger = logging.getLogger(__name__) -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% from %(person)s in the %s room..." +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% from %(person)s " \ + "in the %s room..." MESSAGE_FROM_PERSON = "You have a message on %%app%% from %(person)s..." MESSAGES_FROM_PERSON = "You have messages on %%app%% from %(person)s..." MESSAGES_IN_ROOM = "There are some messages on %%app%% for you in the %(room)s room..." MESSAGES_IN_ROOMS = "Here are some messages on %%app%% you may have missed..." -INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room on %%app%%..." +INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the " \ + "%(room)s room on %%app%%..." INVITE_FROM_PERSON = "%(person)s has invited you to chat on %%app%%..." CONTEXT_BEFORE = 1 -- cgit 1.5.1 From 81c2176cbadf7646fc075ca45dea4360ce7b8258 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 15:54:29 +0100 Subject: fix layout; handle app naming in synapse, not jinja --- res/templates-vector/mail.css | 5 +++- res/templates-vector/notif_mail.html | 4 +-- res/templates-vector/notif_mail.txt | 2 +- res/templates/notif_mail.html | 2 +- res/templates/notif_mail.txt | 2 +- synapse/config/emailconfig.py | 5 ++++ synapse/push/mailer.py | 47 ++++++++++++++++++++++++------------ 7 files changed, 45 insertions(+), 22 deletions(-) (limited to 'synapse/push') diff --git a/res/templates-vector/mail.css b/res/templates-vector/mail.css index 642948fdc1..103c5ff8ca 100644 --- a/res/templates-vector/mail.css +++ b/res/templates-vector/mail.css @@ -85,11 +85,14 @@ body { .sender_name { margin-left: 75px; display: inline; - font-weight: bold; + font-size: 13px; + color: #a2a2a2; } .message_time { float: right; + font-size: 11px; + color: #a2a2a2; } .message_body { diff --git a/res/templates-vector/notif_mail.html b/res/templates-vector/notif_mail.html index 86e7b6e867..c49cc66548 100644 --- a/res/templates-vector/notif_mail.html +++ b/res/templates-vector/notif_mail.html @@ -8,9 +8,9 @@
- +
Hi {{ user_display_name }},
-
{{ summary_text|replace("%app%", "Vector") }}
+
{{ summary_text }}
{% for room in rooms %} diff --git a/res/templates-vector/notif_mail.txt b/res/templates-vector/notif_mail.txt index dec2e5960b..24843042a5 100644 --- a/res/templates-vector/notif_mail.txt +++ b/res/templates-vector/notif_mail.txt @@ -1,6 +1,6 @@ Hi {{ user_display_name }}, -{{ summary_text|replace("%app%", "Vector") }} +{{ summary_text }} {% for room in rooms %} {% include 'room.txt' with context %} diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html index 0290fdea01..cc3573e65a 100644 --- a/res/templates/notif_mail.html +++ b/res/templates/notif_mail.html @@ -8,7 +8,7 @@
Hi {{ user_display_name }},
-
{{ summary_text|replace("%app%", "Matrix") }}
+
{{ summary_text }}
{% for room in rooms %} {% include 'room.html' with context %} diff --git a/res/templates/notif_mail.txt b/res/templates/notif_mail.txt index 5d5a8442f9..24843042a5 100644 --- a/res/templates/notif_mail.txt +++ b/res/templates/notif_mail.txt @@ -1,6 +1,6 @@ Hi {{ user_display_name }}, -{{ summary_text|replace("%app%", "Matrix") }} +{{ summary_text }} {% for room in rooms %} {% include 'room.txt' with context %} diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 7a38680435..d6f4f83a14 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -65,6 +65,10 @@ class EmailConfig(Config): self.email_template_dir = email_config["template_dir"] self.email_notif_template_html = email_config["notif_template_html"] self.email_notif_template_text = email_config["notif_template_text"] + if "app_name" in email_config: + self.email_app_name = email_config["app_name"] + else: + self.email_app_name = "Matrix" # make sure it's valid parsed = email.utils.parseaddr(self.email_notif_from) @@ -83,6 +87,7 @@ class EmailConfig(Config): # smtp_host: "localhost" # smtp_port: 25 # notif_from: Your Friendly Matrix Home Server + # app_name: Matrix # template_dir: res/templates # notif_template_html: notif_mail.html # notif_template_text: notif_mail.txt diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index aab6387125..497905d811 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -39,15 +39,15 @@ import logging logger = logging.getLogger(__name__) -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% from %(person)s " \ +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %(app)s from %(person)s " \ "in the %s room..." -MESSAGE_FROM_PERSON = "You have a message on %%app%% from %(person)s..." -MESSAGES_FROM_PERSON = "You have messages on %%app%% from %(person)s..." -MESSAGES_IN_ROOM = "There are some messages on %%app%% for you in the %(room)s room..." -MESSAGES_IN_ROOMS = "Here are some messages on %%app%% you may have missed..." +MESSAGE_FROM_PERSON = "You have a message on %(app)s from %(person)s..." +MESSAGES_FROM_PERSON = "You have messages on %(app)s from %(person)s..." +MESSAGES_IN_ROOM = "There are some messages on %(app)s for you in the %(room)s room..." +MESSAGES_IN_ROOMS = "Here are some messages on %(app)s you may have missed..." INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the " \ - "%(room)s room on %%app%%..." -INVITE_FROM_PERSON = "%(person)s has invited you to chat on %%app%%..." + "%(room)s room on %(app)s..." +INVITE_FROM_PERSON = "%(person)s has invited you to chat on %(app)s..." CONTEXT_BEFORE = 1 CONTEXT_AFTER = 1 @@ -79,6 +79,7 @@ class Mailer(object): self.store = self.hs.get_datastore() self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) + self.app_name = self.hs.config.email_app_name env = jinja2.Environment(loader=loader) env.filters["format_ts"] = format_ts_filter env.filters["mxc_to_http"] = self.mxc_to_http_filter @@ -306,10 +307,15 @@ class Mailer(object): inviter_name = name_from_member_event(inviter_member_event) if room_name is None: - return INVITE_FROM_PERSON % {"person": inviter_name} + return INVITE_FROM_PERSON % { + "person": inviter_name, + "app": self.app_name + } else: return INVITE_FROM_PERSON_TO_ROOM % { - "person": inviter_name, "room": room_name + "person": inviter_name, + "room": room_name, + "app": self.app_name, } sender_name = None @@ -322,18 +328,22 @@ class Mailer(object): if sender_name is not None and room_name is not None: return MESSAGE_FROM_PERSON_IN_ROOM % { - "person": sender_name, "room": room_name + "person": sender_name, + "room": room_name, + "app": self.app_name, } elif sender_name is not None: return MESSAGE_FROM_PERSON % { - "person": sender_name + "person": sender_name, + "app": self.app_name, } else: # There's more than one notification for this room, so just # say there are several if room_name is not None: return MESSAGES_IN_ROOM % { - "room": room_name + "room": room_name, + "app": self.app_name, } else: # If the room doesn't have a name, say who the messages @@ -347,19 +357,24 @@ class Mailer(object): "person": descriptor_from_member_events([ state_by_room[room_id][("m.room.member", s)] for s in sender_ids - ]) + ]), + "app": self.app_name, } else: # Stuff's happened in multiple different rooms - return MESSAGES_IN_ROOMS + return MESSAGES_IN_ROOMS % { + "app": self.app_name, + } def make_room_link(self, room_id): # XXX: matrix.to - return "https://vector.im/#/room/%s" % (room_id,) + # need /beta for Universal Links to work on iOS + return "https://vector.im/beta/#/room/%s" % (room_id,) def make_notif_link(self, notif): # XXX: matrix.to - return "https://vector.im/#/room/%s/%s" % ( + # need /beta for Universal Links to work on iOS + return "https://vector.im/beta/#/room/%s/%s" % ( notif['room_id'], notif['event_id'] ) -- cgit 1.5.1 From 53ca739f1f3a61f8605dac3f3ab9540dadda0178 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 15:55:44 +0100 Subject: better mail subject lines --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 497905d811..f60ac94a83 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -156,7 +156,7 @@ class Mailer(object): text_part = MIMEText(plain_text, "plain", "utf8") multipart_msg = MIMEMultipart('alternative') - multipart_msg['Subject'] = "New Matrix Notifications" + multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text) multipart_msg['From'] = self.hs.config.email_notif_from multipart_msg['To'] = email_address multipart_msg['Date'] = email.utils.formatdate() -- cgit 1.5.1 From 55996088876664280e7a9232ca21b1875db6c413 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 10 May 2016 00:14:48 +0200 Subject: Switch from CSS to Table layout for HTML mails so they work in Outlook aka Word Remove templates-vector and theme templates with variables instead Switch to matrix.to URLs by default for links --- res/templates-vector/mail.css | 115 ----------------------------- res/templates-vector/notif.html | 34 --------- res/templates-vector/notif.txt | 12 --- res/templates-vector/notif_mail.html | 25 ------- res/templates-vector/notif_mail.txt | 10 --- res/templates-vector/room.html | 27 ------- res/templates-vector/room.txt | 9 --- res/templates/mail.css | 137 +++++++++++++++++++++++++++++++++-- res/templates/notif.html | 50 +++++++------ res/templates/notif.txt | 2 +- res/templates/notif_mail.html | 42 ++++++++--- res/templates/room.html | 54 ++++++++------ synapse/push/mailer.py | 25 +++++-- 13 files changed, 240 insertions(+), 302 deletions(-) delete mode 100644 res/templates-vector/mail.css delete mode 100644 res/templates-vector/notif.html delete mode 100644 res/templates-vector/notif.txt delete mode 100644 res/templates-vector/notif_mail.html delete mode 100644 res/templates-vector/notif_mail.txt delete mode 100644 res/templates-vector/room.html delete mode 100644 res/templates-vector/room.txt (limited to 'synapse/push') diff --git a/res/templates-vector/mail.css b/res/templates-vector/mail.css deleted file mode 100644 index 103c5ff8ca..0000000000 --- a/res/templates-vector/mail.css +++ /dev/null @@ -1,115 +0,0 @@ -body { - margin: 0px; -} - -#page { - font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; - font-size: 12pt; - - margin: auto; - max-width: 640px; - padding: 20px; -} - -.header { - height: 87px; - border-bottom: 4px solid #e4f7ed; -} - -.logo { - float: right; - margin-left: 20px; -} - -.salutation { - padding-top: 10px; - font-weight: bold; -} - -.summarytext { -} - -.room_header { - padding-top: 38px; - padding-bottom: 10px; - border-bottom: 1px solid #e5e5e5; -} - -.room_header h2 { - margin-top: 0px; - margin-left: 75px; - font-size: 20px; -} - -.room_avatar { - float: left; - margin-top: -8px; -} - -.room_avatar img { - width: 48px; - height: 48px; - object-fit: cover; - border-radius: 24px; - margin-left: 7px; -} - -.room_content { - clear: left; -} - -.notif { - border-bottom: 1px solid #e5e5e5; - margin-top: 16px; - padding-bottom: 16px; -} - -.historical { - opacity: 0.3; -} - -.message { - position: relative; - margin-bottom: 10px; -} - -.sender_avatar { - width: 32px; - height: 32px; - border-radius: 16px; - position: absolute; - margin-left: 14px; - margin-top: -2px; -} - -.sender_name { - margin-left: 75px; - display: inline; - font-size: 13px; - color: #a2a2a2; -} - -.message_time { - float: right; - font-size: 11px; - color: #a2a2a2; -} - -.message_body { - margin-left: 75px; -} - -.notif_link { - margin-left: 75px; - font-weight: bold; -} - -.notif_link a, .footer a { - color: #76CFA6; - text-decoration: none; -} - -.footer { - margin-top: 20px; - text-align: center; -} diff --git a/res/templates-vector/notif.html b/res/templates-vector/notif.html deleted file mode 100644 index 97ea425011..0000000000 --- a/res/templates-vector/notif.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
- {% for message in notif.messages %} -
- {% if message.sender_avatar_url %} - - {% else %} - {% if message.sender_hash % 3 == 0 %} - - {% elif message.sender_hash % 3 == 1 %} - - {% else %} - - {% endif %} - - {% endif %} -
{{ message.sender_name }}
-
{{ message.ts|format_ts("%H:%M") }}
-
- {% if message.msgtype == "m.text" %} - {{ message.body_text_html }} - {% elif message.msgtype == "m.image" %} - - {% elif message.msgtype == "m.file" %} - {{ message.body_text_plain }} - {% endif %} -
-
- {% endfor %} -
- -
diff --git a/res/templates-vector/notif.txt b/res/templates-vector/notif.txt deleted file mode 100644 index b515f394c3..0000000000 --- a/res/templates-vector/notif.txt +++ /dev/null @@ -1,12 +0,0 @@ -{% for message in notif.messages %} -{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }}) -{% if message.msgtype == "m.text" %} -{{ message.body_text_plain }} -{% elif message.msgtype == "m.image" %} -{{ message.body_text_plain }} -{% elif message.msgtype == "m.file" %} -{{ message.body_text_plain }} -{% endif %} -{% endfor %} - -View at {{ notif.link }} diff --git a/res/templates-vector/notif_mail.html b/res/templates-vector/notif_mail.html deleted file mode 100644 index c49cc66548..0000000000 --- a/res/templates-vector/notif_mail.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - -
-
- -
Hi {{ user_display_name }},
-
{{ summary_text }}
-
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
- -
- - diff --git a/res/templates-vector/notif_mail.txt b/res/templates-vector/notif_mail.txt deleted file mode 100644 index 24843042a5..0000000000 --- a/res/templates-vector/notif_mail.txt +++ /dev/null @@ -1,10 +0,0 @@ -Hi {{ user_display_name }}, - -{{ summary_text }} - -{% for room in rooms %} -{% include 'room.txt' with context %} -{% endfor %} - -You can disable these notifications at {{ unsubscribe_link }} - diff --git a/res/templates-vector/room.html b/res/templates-vector/room.html deleted file mode 100644 index ab11492578..0000000000 --- a/res/templates-vector/room.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
- {% if room.avatar_url %} - - {% else %} - {% if room.hash % 3 == 0 %} - - {% elif room.hash % 3 == 1 %} - - {% else %} - - {% endif %} - {% endif %} -
-

{{ room.title }}

-
-
- {% if room.invite %} - Join the conversation. - {% else %} - {% for notif in room.notifs %} - {% include 'notif.html' with context %} - {% endfor %} - {% endif %} -
-
diff --git a/res/templates-vector/room.txt b/res/templates-vector/room.txt deleted file mode 100644 index 84648c710e..0000000000 --- a/res/templates-vector/room.txt +++ /dev/null @@ -1,9 +0,0 @@ -{{ room.title }} - -{% if room.invite %} - You've been invited, join at {{ room.link }} -{% else %} - {% for notif in room.notifs %} - {% include 'notif.txt' with context %} - {% endfor %} -{% endif %} diff --git a/res/templates/mail.css b/res/templates/mail.css index 61953ba51c..b02f509e58 100644 --- a/res/templates/mail.css +++ b/res/templates/mail.css @@ -1,21 +1,146 @@ +body { + margin: 0px; +} + +#page { + font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; + font-color: #454545; + font-size: 12pt; + width: 100%; + padding: 20px; +} + +#inner { + width: 640px; +} + +.header { + width: 100%; + height: 87px; + color: #454545; + border-bottom: 4px solid #e5e5e5; +} + +.logo { + text-align: right; + margin-left: 20px; +} + +.salutation { + padding-top: 10px; + font-weight: bold; +} + +.summarytext { +} + +.room { + width: 100%; + color: #454545; + border-bottom: 1px solid #e5e5e5; +} + +.room_header td { + padding-top: 38px; + padding-bottom: 10px; + border-bottom: 1px solid #e5e5e5; +} + +.room_name { + vertical-align: middle; + font-size: 18px; + font-weight: bold; +} + +.room_header h2 { + margin-top: 0px; + margin-left: 75px; + font-size: 20px; +} + .room_avatar { + width: 56px; + line-height: 0px; + text-align: center; + vertical-align: middle; +} + +.room_avatar img { width: 48px; height: 48px; - float: left; + object-fit: cover; + border-radius: 24px; +} + +.notif { + border-bottom: 1px solid #e5e5e5; + margin-top: 16px; + padding-bottom: 16px; +} + +.historical_message .sender_avatar { + opacity: 0.3; +} + +/* spell out opacity and historical_message class names for Outlook aka Word */ +.historical_message .sender_name { + color: #e3e3e3; +} + +.historical_message .message_time { + color: #e3e3e3; +} + +.historical_message .message_body { + color: #c7c7c7; +} + +.historical_message td, +.message td { + padding-top: 10px; } -.room_content { - clear: left; +.sender_avatar { + width: 56px; + text-align: center; + vertical-align: top; } -.historical { - color: #888; +.sender_avatar img { + margin-top: -2px; + width: 32px; + height: 32px; + border-radius: 16px; } .sender_name { display: inline; + font-size: 13px; + color: #a2a2a2; } .message_time { - display: inline; + text-align: right; + width: 100px; + font-size: 11px; + color: #a2a2a2; +} + +.message_body { } + +.notif_link td { + padding-top: 10px; + padding-bottom: 10px; + font-weight: bold; +} + +.notif_link a, .footer a { + color: #454545; + text-decoration: none; +} + +.footer { + margin-top: 20px; + text-align: center; +} \ No newline at end of file diff --git a/res/templates/notif.html b/res/templates/notif.html index 3112df9704..92e80352fc 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -1,9 +1,9 @@ -
-
- {% for message in notif.messages %} -
+{% for message in notif.messages %} + + + {% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %} {% if message.sender_avatar_url %} - + {% else %} {% if message.sender_hash % 3 == 0 %} @@ -14,21 +14,29 @@ {% endif %} {% endif %} + {% endif %} + + + {% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
{{ message.sender_name }}
-
{{ message.ts|format_ts("%H:%M") }}
-
- {% if message.msgtype == "m.text" %} - {{ message.body_text_html }} - {% elif message.msgtype == "m.image" %} - - {% elif message.msgtype == "m.file" %} - {{ message.body_text_plain }} - {% endif %} -
+ {% endif %} +
+ {% if message.msgtype == "m.text" %} + {{ message.body_text_html }} + {% elif message.msgtype == "m.image" %} + + {% elif message.msgtype == "m.file" %} + {{ message.body_text_plain }} + {% endif %}
- {% endfor %} -
- -
+ + {{ message.ts|format_ts("%H:%M") }} + +{% endfor %} + + + + View {{ room.title }} + + + diff --git a/res/templates/notif.txt b/res/templates/notif.txt index b515f394c3..a3ddac80ce 100644 --- a/res/templates/notif.txt +++ b/res/templates/notif.txt @@ -9,4 +9,4 @@ {% endif %} {% endfor %} -View at {{ notif.link }} +View {{ room.title }} at {{ notif.link }} diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html index cc3573e65a..4034f3c601 100644 --- a/res/templates/notif_mail.html +++ b/res/templates/notif_mail.html @@ -3,20 +3,38 @@ -
-
Hi {{ user_display_name }},
-
{{ summary_text }}
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
- -
+ + + + + + +
+ + + + + +
+
Hi {{ user_display_name }},
+
{{ summary_text }}
+
+ {% for room in rooms %} + {% include 'room.html' with context %} + {% endfor %} + +
diff --git a/res/templates/room.html b/res/templates/room.html index 3c0a4607b3..723c222d25 100644 --- a/res/templates/room.html +++ b/res/templates/room.html @@ -1,25 +1,33 @@ -
-
- {% if room.avatar_url %} - - {% else %} - {% if room.hash % 3 == 0 %} - - {% elif room.hash % 3 == 1 %} - + + + + + + {% if room.invite %} + + + + + + {% else %} + {% for notif in room.notifs %} + {% include 'notif.html' with context %} + {% endfor %} + {% endif %} +
+ {% if room.avatar_url %} + {% else %} - + {% if room.hash % 3 == 0 %} + + {% elif room.hash % 3 == 1 %} + + {% else %} + + {% endif %} {% endif %} - {% endif %} - -

{{ room.title }}

-
- {% if room.invite %} - Join the conversation. - {% else %} - {% for notif in room.notifs %} - {% include 'notif.html' with context %} - {% endfor %} - {% endif %} -
- +
+ {{ room.title }} +
+ Join the conversation. +
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index f60ac94a83..3c38321fdd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -146,6 +146,7 @@ class Mailer(object): "user_display_name": user_display_name, "unsubscribe_link": self.make_unsubscribe_link(), "summary_text": summary_text, + "app_name": self.app_name, "rooms": rooms, } @@ -164,6 +165,9 @@ class Mailer(object): multipart_msg.attach(text_part) multipart_msg.attach(html_part) + logger.info("Sending email push notification to %s" % email_address) + #logger.debug(html_text) + yield sendmail( self.hs.config.email_smtp_host, raw_from, raw_to, multipart_msg.as_string(), @@ -367,19 +371,26 @@ class Mailer(object): } def make_room_link(self, room_id): - # XXX: matrix.to # need /beta for Universal Links to work on iOS - return "https://vector.im/beta/#/room/%s" % (room_id,) + if self.app_name == "Vector": + return "https://vector.im/beta/#/room/%s" % (room_id,) + else: + return "https://matrix.to/#/room/%s" % (room_id,) def make_notif_link(self, notif): - # XXX: matrix.to # need /beta for Universal Links to work on iOS - return "https://vector.im/beta/#/room/%s/%s" % ( - notif['room_id'], notif['event_id'] - ) + if self.app_name == "Vector": + return "https://vector.im/beta/#/room/%s/%s" % ( + notif['room_id'], notif['event_id'] + ) + else: + return "https://matrix.to/#/room/%s/%s" % ( + notif['room_id'], notif['event_id'] + ) def make_unsubscribe_link(self): - return "https://vector.im/#/settings" # XXX: matrix.to + # XXX: matrix.to + return "https://vector.im/#/settings" def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": -- cgit 1.5.1 From e04b1d6b0a6b6f2934e59e73a605e99da6ca9f5e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 May 2016 14:23:16 +0200 Subject: Make pep8 happy --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 3c38321fdd..7031fa6d55 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -166,7 +166,7 @@ class Mailer(object): multipart_msg.attach(html_part) logger.info("Sending email push notification to %s" % email_address) - #logger.debug(html_text) + # logger.debug(html_text) yield sendmail( self.hs.config.email_smtp_host, -- cgit 1.5.1 From 7e23476814b2cd3c8cd8ef87d2b312fbca400da6 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 11 May 2016 13:42:37 +0100 Subject: move filter_events_for_client out of base handler --- synapse/handlers/_base.py | 184 --------------------------- synapse/handlers/message.py | 18 +-- synapse/handlers/room.py | 7 +- synapse/handlers/search.py | 17 +-- synapse/handlers/sync.py | 7 +- synapse/notifier.py | 5 +- synapse/push/action_generator.py | 4 +- synapse/push/bulk_push_rule_evaluator.py | 7 +- synapse/push/mailer.py | 6 +- synapse/visibility.py | 210 +++++++++++++++++++++++++++++++ 10 files changed, 251 insertions(+), 214 deletions(-) create mode 100644 synapse/visibility.py (limited to 'synapse/push') diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index ac716a8118..c904c6c500 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -19,7 +19,6 @@ from synapse.api.errors import LimitExceededError from synapse.api.constants import Membership, EventTypes from synapse.types import UserID, Requester -from synapse.util.logcontext import preserve_fn import logging @@ -27,23 +26,6 @@ import logging logger = logging.getLogger(__name__) -VISIBILITY_PRIORITY = ( - "world_readable", - "shared", - "invited", - "joined", -) - - -MEMBERSHIP_PRIORITY = ( - Membership.JOIN, - Membership.INVITE, - Membership.KNOCK, - Membership.LEAVE, - Membership.BAN, -) - - class BaseHandler(object): """ Common base class for the event handlers. @@ -67,172 +49,6 @@ class BaseHandler(object): self.event_builder_factory = hs.get_event_builder_factory() - @defer.inlineCallbacks - def filter_events_for_clients(self, user_tuples, events, event_id_to_state): - """ Returns dict of user_id -> list of events that user is allowed to - see. - - Args: - user_tuples (str, bool): (user id, is_peeking) for each user to be - checked. is_peeking should be true if: - * the user is not currently a member of the room, and: - * the user has not been a member of the room since the - given events - events ([synapse.events.EventBase]): list of events to filter - """ - forgotten = yield defer.gatherResults([ - preserve_fn(self.store.who_forgot_in_room)( - room_id, - ) - for room_id in frozenset(e.room_id for e in events) - ], consumeErrors=True) - - # Set of membership event_ids that have been forgotten - event_id_forgotten = frozenset( - row["event_id"] for rows in forgotten for row in rows - ) - - ignore_dict_content = yield self.store.get_global_account_data_by_type_for_users( - "m.ignored_user_list", user_ids=[user_id for user_id, _ in user_tuples] - ) - - # FIXME: This will explode if people upload something incorrect. - ignore_dict = { - user_id: frozenset( - content.get("ignored_users", {}).keys() if content else [] - ) - for user_id, content in ignore_dict_content.items() - } - - def allowed(event, user_id, is_peeking, ignore_list): - """ - Args: - event (synapse.events.EventBase): event to check - user_id (str) - is_peeking (bool) - ignore_list (list): list of users to ignore - """ - if not event.is_state() and event.sender in ignore_list: - return False - - state = event_id_to_state[event.event_id] - - # get the room_visibility at the time of the event. - visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None) - if visibility_event: - visibility = visibility_event.content.get("history_visibility", "shared") - else: - visibility = "shared" - - if visibility not in VISIBILITY_PRIORITY: - visibility = "shared" - - # if it was world_readable, it's easy: everyone can read it - if visibility == "world_readable": - return True - - # Always allow history visibility events on boundaries. This is done - # by setting the effective visibility to the least restrictive - # of the old vs new. - if event.type == EventTypes.RoomHistoryVisibility: - prev_content = event.unsigned.get("prev_content", {}) - prev_visibility = prev_content.get("history_visibility", None) - - if prev_visibility not in VISIBILITY_PRIORITY: - prev_visibility = "shared" - - new_priority = VISIBILITY_PRIORITY.index(visibility) - old_priority = VISIBILITY_PRIORITY.index(prev_visibility) - if old_priority < new_priority: - visibility = prev_visibility - - # likewise, if the event is the user's own membership event, use - # the 'most joined' membership - membership = None - if event.type == EventTypes.Member and event.state_key == user_id: - membership = event.content.get("membership", None) - if membership not in MEMBERSHIP_PRIORITY: - membership = "leave" - - prev_content = event.unsigned.get("prev_content", {}) - prev_membership = prev_content.get("membership", None) - if prev_membership not in MEMBERSHIP_PRIORITY: - prev_membership = "leave" - - new_priority = MEMBERSHIP_PRIORITY.index(membership) - old_priority = MEMBERSHIP_PRIORITY.index(prev_membership) - if old_priority < new_priority: - membership = prev_membership - - # otherwise, get the user's membership at the time of the event. - if membership is None: - membership_event = state.get((EventTypes.Member, user_id), None) - if membership_event: - if membership_event.event_id not in event_id_forgotten: - membership = membership_event.membership - - # if the user was a member of the room at the time of the event, - # they can see it. - if membership == Membership.JOIN: - return True - - if visibility == "joined": - # we weren't a member at the time of the event, so we can't - # see this event. - return False - - elif visibility == "invited": - # user can also see the event if they were *invited* at the time - # of the event. - return membership == Membership.INVITE - - else: - # visibility is shared: user can also see the event if they have - # become a member since the event - # - # XXX: if the user has subsequently joined and then left again, - # ideally we would share history up to the point they left. But - # we don't know when they left. - return not is_peeking - - defer.returnValue({ - user_id: [ - event - for event in events - if allowed(event, user_id, is_peeking, ignore_dict.get(user_id, [])) - ] - for user_id, is_peeking in user_tuples - }) - - @defer.inlineCallbacks - def filter_events_for_client(self, user_id, events, is_peeking=False): - """ - Check which events a user is allowed to see - - Args: - user_id(str): user id to be checked - events([synapse.events.EventBase]): list of events to be checked - is_peeking(bool): should be True if: - * the user is not currently a member of the room, and: - * the user has not been a member of the room since the given - events - - Returns: - [synapse.events.EventBase] - """ - types = ( - (EventTypes.RoomHistoryVisibility, ""), - (EventTypes.Member, user_id), - ) - event_id_to_state = yield self.store.get_state_for_events( - frozenset(e.event_id for e in events), - types=types - ) - res = yield self.filter_events_for_clients( - [(user_id, is_peeking)], events, event_id_to_state - ) - defer.returnValue(res.get(user_id, [])) - def ratelimit(self, requester): time_now = self.clock.time() allowed, time_allowed = self.ratelimiter.send_message( diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index f9e2c98f3f..13154edb78 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -29,6 +29,7 @@ from synapse.util import unwrapFirstError from synapse.util.async import concurrently_execute from synapse.util.caches.snapshot_cache import SnapshotCache from synapse.util.logcontext import PreserveLoggingContext, preserve_fn +from synapse.visibility import filter_events_for_client from ._base import BaseHandler @@ -128,7 +129,8 @@ class MessageHandler(BaseHandler): "end": next_token.to_string(), }) - events = yield self.filter_events_for_client( + events = yield filter_events_for_client( + self.store, user_id, events, is_peeking=(member_event_id is None), @@ -488,8 +490,8 @@ class MessageHandler(BaseHandler): ] ).addErrback(unwrapFirstError) - messages = yield self.filter_events_for_client( - user_id, messages + messages = yield filter_events_for_client( + self.store, user_id, messages ) start_token = now_token.copy_and_replace("room_key", token[0]) @@ -624,8 +626,8 @@ class MessageHandler(BaseHandler): end_token=stream_token ) - messages = yield self.filter_events_for_client( - user_id, messages, is_peeking=is_peeking + messages = yield filter_events_for_client( + self.store, user_id, messages, is_peeking=is_peeking ) start_token = StreamToken.START.copy_and_replace("room_key", token[0]) @@ -705,8 +707,8 @@ class MessageHandler(BaseHandler): consumeErrors=True, ).addErrback(unwrapFirstError) - messages = yield self.filter_events_for_client( - user_id, messages, is_peeking=is_peeking, + messages = yield filter_events_for_client( + self.store, user_id, messages, is_peeking=is_peeking, ) start_token = now_token.copy_and_replace("room_key", token[0]) @@ -882,7 +884,7 @@ class MessageHandler(BaseHandler): action_generator = ActionGenerator(self.hs) yield action_generator.handle_push_actions_for_event( - event, context, self + event, context ) (event_stream_id, max_stream_id) = yield self.store.persist_event( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index fdebc9c438..3d63b3c513 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -26,6 +26,7 @@ from synapse.api.errors import AuthError, StoreError, SynapseError from synapse.util import stringutils from synapse.util.async import concurrently_execute from synapse.util.caches.response_cache import ResponseCache +from synapse.visibility import filter_events_for_client from collections import OrderedDict @@ -449,10 +450,12 @@ class RoomContextHandler(BaseHandler): now_token = yield self.hs.get_event_sources().get_current_token() def filter_evts(events): - return self.filter_events_for_client( + return filter_events_for_client( + self.store, user.to_string(), events, - is_peeking=is_guest) + is_peeking=is_guest + ) event = yield self.store.get_event(event_id, get_prev_content=True, allow_none=True) diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index a937e87408..df75d70fac 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -21,6 +21,7 @@ from synapse.api.constants import Membership, EventTypes from synapse.api.filtering import Filter from synapse.api.errors import SynapseError from synapse.events.utils import serialize_event +from synapse.visibility import filter_events_for_client from unpaddedbase64 import decode_base64, encode_base64 @@ -172,8 +173,8 @@ class SearchHandler(BaseHandler): filtered_events = search_filter.filter([r["event"] for r in results]) - events = yield self.filter_events_for_client( - user.to_string(), filtered_events + events = yield filter_events_for_client( + self.store, user.to_string(), filtered_events ) events.sort(key=lambda e: -rank_map[e.event_id]) @@ -223,8 +224,8 @@ class SearchHandler(BaseHandler): r["event"] for r in results ]) - events = yield self.filter_events_for_client( - user.to_string(), filtered_events + events = yield filter_events_for_client( + self.store, user.to_string(), filtered_events ) room_events.extend(events) @@ -281,12 +282,12 @@ class SearchHandler(BaseHandler): event.room_id, event.event_id, before_limit, after_limit ) - res["events_before"] = yield self.filter_events_for_client( - user.to_string(), res["events_before"] + res["events_before"] = yield filter_events_for_client( + self.store, user.to_string(), res["events_before"] ) - res["events_after"] = yield self.filter_events_for_client( - user.to_string(), res["events_after"] + res["events_after"] = yield filter_events_for_client( + self.store, user.to_string(), res["events_after"] ) res["start"] = now_token.copy_and_replace( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index b7dcbc6b1b..921215469f 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -22,6 +22,7 @@ from synapse.util.logcontext import LoggingContext from synapse.util.metrics import Measure from synapse.util.caches.response_cache import ResponseCache from synapse.push.clientformat import format_push_rules_for_user +from synapse.visibility import filter_events_for_client from twisted.internet import defer @@ -697,7 +698,8 @@ class SyncHandler(BaseHandler): if recents is not None: recents = sync_config.filter_collection.filter_room_timeline(recents) - recents = yield self.filter_events_for_client( + recents = yield filter_events_for_client( + self.store, sync_config.user.to_string(), recents, ) @@ -718,7 +720,8 @@ class SyncHandler(BaseHandler): loaded_recents = sync_config.filter_collection.filter_room_timeline( events ) - loaded_recents = yield self.filter_events_for_client( + loaded_recents = yield filter_events_for_client( + self.store, sync_config.user.to_string(), loaded_recents, ) diff --git a/synapse/notifier.py b/synapse/notifier.py index cb58dfffd4..33b79c0ec7 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -21,6 +21,7 @@ from synapse.util.logutils import log_function from synapse.util.async import ObservableDeferred from synapse.util.logcontext import PreserveLoggingContext from synapse.types import StreamToken +from synapse.visibility import filter_events_for_client import synapse.metrics from collections import namedtuple @@ -398,8 +399,8 @@ class Notifier(object): ) if name == "room": - room_member_handler = self.hs.get_handlers().room_member_handler - new_events = yield room_member_handler.filter_events_for_client( + new_events = yield filter_events_for_client( + self.store, user.to_string(), new_events, is_peeking=is_peeking, diff --git a/synapse/push/action_generator.py b/synapse/push/action_generator.py index a0160994b7..9b208668b6 100644 --- a/synapse/push/action_generator.py +++ b/synapse/push/action_generator.py @@ -37,14 +37,14 @@ class ActionGenerator: # tag (ie. we just need all the users). @defer.inlineCallbacks - def handle_push_actions_for_event(self, event, context, handler): + def handle_push_actions_for_event(self, event, context): with Measure(self.clock, "handle_push_actions_for_event"): bulk_evaluator = yield evaluator_for_event( event, self.hs, self.store ) actions_by_user = yield bulk_evaluator.action_for_event_by_user( - event, handler, context.current_state + event, context.current_state ) context.push_actions = [ diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index f97df36d80..25e13b3423 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -22,6 +22,7 @@ from .baserules import list_with_base_rules from .push_rule_evaluator import PushRuleEvaluatorForEvent from synapse.api.constants import EventTypes +from synapse.visibility import filter_events_for_clients logger = logging.getLogger(__name__) @@ -126,7 +127,7 @@ class BulkPushRuleEvaluator: self.store = store @defer.inlineCallbacks - def action_for_event_by_user(self, event, handler, current_state): + def action_for_event_by_user(self, event, current_state): actions_by_user = {} # None of these users can be peeking since this list of users comes @@ -136,8 +137,8 @@ class BulkPushRuleEvaluator: (u, False) for u in self.rules_by_user.keys() ] - filtered_by_user = yield handler.filter_events_for_clients( - user_tuples, [event], {event.event_id: current_state} + filtered_by_user = yield filter_events_for_clients( + self.store, user_tuples, [event], {event.event_id: current_state} ) room_members = yield self.store.get_users_in_room(self.room_id) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 7031fa6d55..5d60c1efcf 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -28,6 +28,7 @@ from synapse.util.presentable_names import ( from synapse.types import UserID from synapse.api.errors import StoreError from synapse.api.constants import EventTypes +from synapse.visibility import filter_events_for_client import jinja2 import bleach @@ -227,9 +228,8 @@ class Mailer(object): "messages": [], } - handler = self.hs.get_handlers().message_handler - the_events = yield handler.filter_events_for_client( - user_id, results["events_before"] + the_events = yield filter_events_for_client( + self.store, user_id, results["events_before"] ) the_events.append(notif_event) diff --git a/synapse/visibility.py b/synapse/visibility.py new file mode 100644 index 0000000000..948ad51772 --- /dev/null +++ b/synapse/visibility.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 - 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.api.constants import Membership, EventTypes + +from synapse.util.logcontext import preserve_fn + +import logging + + +logger = logging.getLogger(__name__) + + +VISIBILITY_PRIORITY = ( + "world_readable", + "shared", + "invited", + "joined", +) + + +MEMBERSHIP_PRIORITY = ( + Membership.JOIN, + Membership.INVITE, + Membership.KNOCK, + Membership.LEAVE, + Membership.BAN, +) + + +@defer.inlineCallbacks +def filter_events_for_clients(store, user_tuples, events, event_id_to_state): + """ Returns dict of user_id -> list of events that user is allowed to + see. + + Args: + user_tuples (str, bool): (user id, is_peeking) for each user to be + checked. is_peeking should be true if: + * the user is not currently a member of the room, and: + * the user has not been a member of the room since the + given events + events ([synapse.events.EventBase]): list of events to filter + """ + forgotten = yield defer.gatherResults([ + preserve_fn(store.who_forgot_in_room)( + room_id, + ) + for room_id in frozenset(e.room_id for e in events) + ], consumeErrors=True) + + # Set of membership event_ids that have been forgotten + event_id_forgotten = frozenset( + row["event_id"] for rows in forgotten for row in rows + ) + + ignore_dict_content = yield store.get_global_account_data_by_type_for_users( + "m.ignored_user_list", user_ids=[user_id for user_id, _ in user_tuples] + ) + + # FIXME: This will explode if people upload something incorrect. + ignore_dict = { + user_id: frozenset( + content.get("ignored_users", {}).keys() if content else [] + ) + for user_id, content in ignore_dict_content.items() + } + + def allowed(event, user_id, is_peeking, ignore_list): + """ + Args: + event (synapse.events.EventBase): event to check + user_id (str) + is_peeking (bool) + ignore_list (list): list of users to ignore + """ + if not event.is_state() and event.sender in ignore_list: + return False + + state = event_id_to_state[event.event_id] + + # get the room_visibility at the time of the event. + visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None) + if visibility_event: + visibility = visibility_event.content.get("history_visibility", "shared") + else: + visibility = "shared" + + if visibility not in VISIBILITY_PRIORITY: + visibility = "shared" + + # if it was world_readable, it's easy: everyone can read it + if visibility == "world_readable": + return True + + # Always allow history visibility events on boundaries. This is done + # by setting the effective visibility to the least restrictive + # of the old vs new. + if event.type == EventTypes.RoomHistoryVisibility: + prev_content = event.unsigned.get("prev_content", {}) + prev_visibility = prev_content.get("history_visibility", None) + + if prev_visibility not in VISIBILITY_PRIORITY: + prev_visibility = "shared" + + new_priority = VISIBILITY_PRIORITY.index(visibility) + old_priority = VISIBILITY_PRIORITY.index(prev_visibility) + if old_priority < new_priority: + visibility = prev_visibility + + # likewise, if the event is the user's own membership event, use + # the 'most joined' membership + membership = None + if event.type == EventTypes.Member and event.state_key == user_id: + membership = event.content.get("membership", None) + if membership not in MEMBERSHIP_PRIORITY: + membership = "leave" + + prev_content = event.unsigned.get("prev_content", {}) + prev_membership = prev_content.get("membership", None) + if prev_membership not in MEMBERSHIP_PRIORITY: + prev_membership = "leave" + + new_priority = MEMBERSHIP_PRIORITY.index(membership) + old_priority = MEMBERSHIP_PRIORITY.index(prev_membership) + if old_priority < new_priority: + membership = prev_membership + + # otherwise, get the user's membership at the time of the event. + if membership is None: + membership_event = state.get((EventTypes.Member, user_id), None) + if membership_event: + if membership_event.event_id not in event_id_forgotten: + membership = membership_event.membership + + # if the user was a member of the room at the time of the event, + # they can see it. + if membership == Membership.JOIN: + return True + + if visibility == "joined": + # we weren't a member at the time of the event, so we can't + # see this event. + return False + + elif visibility == "invited": + # user can also see the event if they were *invited* at the time + # of the event. + return membership == Membership.INVITE + + else: + # visibility is shared: user can also see the event if they have + # become a member since the event + # + # XXX: if the user has subsequently joined and then left again, + # ideally we would share history up to the point they left. But + # we don't know when they left. + return not is_peeking + + defer.returnValue({ + user_id: [ + event + for event in events + if allowed(event, user_id, is_peeking, ignore_dict.get(user_id, [])) + ] + for user_id, is_peeking in user_tuples + }) + + +@defer.inlineCallbacks +def filter_events_for_client(store, user_id, events, is_peeking=False): + """ + Check which events a user is allowed to see + + Args: + user_id(str): user id to be checked + events([synapse.events.EventBase]): list of events to be checked + is_peeking(bool): should be True if: + * the user is not currently a member of the room, and: + * the user has not been a member of the room since the given + events + + Returns: + [synapse.events.EventBase] + """ + types = ( + (EventTypes.RoomHistoryVisibility, ""), + (EventTypes.Member, user_id), + ) + event_id_to_state = yield store.get_state_for_events( + frozenset(e.event_id for e in events), + types=types + ) + res = yield filter_events_for_clients( + store, [(user_id, is_peeking)], events, event_id_to_state + ) + defer.returnValue(res.get(user_id, [])) -- cgit 1.5.1 From b5e646a18ce2a293e5d35dcb560ba50183a87429 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 May 2016 11:36:50 +0100 Subject: Make email notifs work on the pusher synapse Plus general bugfix to email notif code --- synapse/app/pusher.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ synapse/push/mailer.py | 1 + 2 files changed, 48 insertions(+) (limited to 'synapse/push') diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 8e9c0e1960..662cd0dc6b 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -24,6 +24,8 @@ from synapse.config.emailconfig import EmailConfig from synapse.http.site import SynapseSite from synapse.metrics.resource import MetricsResource, METRICS_PREFIX from synapse.storage.state import StateStore +from synapse.storage.roommember import RoomMemberStore +from synapse.storage.account_data import AccountDataStore from synapse.replication.slave.storage.events import SlavedEventStore from synapse.replication.slave.storage.pushers import SlavedPusherStore from synapse.replication.slave.storage.receipts import SlavedReceiptsStore @@ -60,6 +62,7 @@ class SlaveConfig(DatabaseConfig): self.soft_file_limit = config.get("soft_file_limit") self.daemonize = config.get("daemonize") self.pid_file = self.abspath(config.get("pid_file")) + self.public_baseurl = config["public_baseurl"] def default_config(self, server_name, **kwargs): pid_file = self.abspath("pusher.pid") @@ -132,6 +135,30 @@ class PusherSlaveStore( DataStore.get_state_groups.__func__ ) + _get_state_for_groups = ( + DataStore._get_state_for_groups.__func__ + ) + + _get_all_state_from_cache = ( + DataStore._get_all_state_from_cache.__func__ + ) + + get_events_around = ( + DataStore.get_events_around.__func__ + ) + + _get_events_around_txn = ( + DataStore._get_events_around_txn.__func__ + ) + + get_state_for_events = ( + DataStore.get_state_for_events.__func__ + ) + + _get_some_state_from_cache = ( + DataStore._get_some_state_from_cache.__func__ + ) + _get_state_group_for_events = ( StateStore.__dict__["_get_state_group_for_events"] ) @@ -140,6 +167,26 @@ class PusherSlaveStore( StateStore.__dict__["_get_state_group_for_event"] ) + _get_state_groups_from_groups = ( + StateStore.__dict__["_get_state_groups_from_groups"] + ) + + _get_state_group_from_group = ( + StateStore.__dict__["_get_state_group_from_group"] + ) + + get_global_account_data_by_type_for_users = ( + AccountDataStore.__dict__["get_global_account_data_by_type_for_users"] + ) + + get_global_account_data_by_type_for_user = ( + AccountDataStore.__dict__["get_global_account_data_by_type_for_user"] + ) + + who_forgot_in_room = ( + RoomMemberStore.__dict__["who_forgot_in_room"] + ) + class PusherServer(HomeServer): diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 5d60c1efcf..2be294f52e 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -397,6 +397,7 @@ class Mailer(object): return "" serverAndMediaId = value[6:] + fragment = None if '#' in serverAndMediaId: (serverAndMediaId, fragment) = serverAndMediaId.split('#', 1) fragment = "#" + fragment -- cgit 1.5.1 From 782471b7e110165697fd3de19739802852cc2a91 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 13 May 2016 17:49:53 +0100 Subject: fix matrix.to URLs --- synapse/push/mailer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 2be294f52e..2fd38a036a 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -375,7 +375,7 @@ class Mailer(object): if self.app_name == "Vector": return "https://vector.im/beta/#/room/%s" % (room_id,) else: - return "https://matrix.to/#/room/%s" % (room_id,) + return "https://matrix.to/#/%s" % (room_id,) def make_notif_link(self, notif): # need /beta for Universal Links to work on iOS @@ -384,7 +384,7 @@ class Mailer(object): notif['room_id'], notif['event_id'] ) else: - return "https://matrix.to/#/room/%s/%s" % ( + return "https://matrix.to/#/%s/%s" % ( notif['room_id'], notif['event_id'] ) -- cgit 1.5.1 From 647781ca56c5378250605bdd3c1d69816be03e72 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 May 2016 18:40:29 +0100 Subject: Fix emailpusher import Try importing at the root level rather than conditionally importing, as per comment --- synapse/push/pusher.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index e6c0806415..de9c33b936 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -18,6 +18,17 @@ from httppusher import HttpPusher import logging logger = logging.getLogger(__name__) +# We try importing this if we can (it will fail if we don't +# have the optional email dependencies installed). We don't +# yet have the config to know if we need the email pusher, +# but importing this after daemonizing seems to fail +# (even though a simple test of importing from a daemonized +# process works fine) +try: + from synapse.push.emailpusher import EmailPusher +except: + pass + def create_pusher(hs, pusherdict): logger.info("trying to create_pusher for %r", pusherdict) @@ -28,7 +39,6 @@ def create_pusher(hs, pusherdict): logger.info("email enable notifs: %r", hs.config.email_enable_notifs) if hs.config.email_enable_notifs: - from synapse.push.emailpusher import EmailPusher PUSHER_TYPES["email"] = EmailPusher logger.info("defined email pusher type") -- cgit 1.5.1 From cbd2adc95e793cb232263efd05b025d6098356af Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 16 May 2016 18:58:38 +0100 Subject: tune email notifs, fix CSS a bit, and add debugging details --- res/templates/mail.css | 5 +++++ res/templates/notif_mail.html | 11 +++++++++++ synapse/push/emailpusher.py | 32 ++++++++++++++++++++++++-------- synapse/push/mailer.py | 7 ++++++- 4 files changed, 46 insertions(+), 9 deletions(-) (limited to 'synapse/push') diff --git a/res/templates/mail.css b/res/templates/mail.css index b02f509e58..f2b5e84abc 100644 --- a/res/templates/mail.css +++ b/res/templates/mail.css @@ -2,6 +2,11 @@ body { margin: 0px; } +pre, code { + word-break: break-word; + white-space: pre-wrap; +} + #page { font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; font-color: #454545; diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html index 4034f3c601..ba65a80291 100644 --- a/res/templates/notif_mail.html +++ b/res/templates/notif_mail.html @@ -30,6 +30,17 @@ {% include 'room.html' with context %} {% endfor %} diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 3a13c7485a..b4b728adc5 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -26,11 +26,14 @@ logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification # (to give the user a chance to respond to other push or notice the window) -DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 +DELAY_BEFORE_MAIL_MS = 10 * 60 * 1000 -THROTTLE_START_MS = 2 * 60 * 1000 -THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days -THROTTLE_MULTIPLIER = 2 +# THROTTLE is the minimum time between mail notifications sent for a given room. +# Each room maintains its own throttle counter, but each new mail notification +# sends the pending notifications for all rooms. +THROTTLE_START_MS = 10 * 60 * 1000 +THROTTLE_MAX_MS = 24 * 60 * 60 * 1000 # (2 * 60 * 1000) * (2 ** 11) # ~3 days +THROTTLE_MULTIPLIER = 6 # 10 mins, 1 hour, 6 hours, 24 hours # If no event triggers a notification for this long after the previous, # the throttle is released. @@ -146,7 +149,18 @@ class EmailPusher(object): # *one* email updating the user on their notifications, # we then consider all previously outstanding notifications # to be delivered. - yield self.send_notification(unprocessed) + + # debugging: + reason = { + 'room_id': push_action['room_id'], + 'now': self.clock.time_msec(), + 'received_at': received_at, + 'delay_before_mail_ms': DELAY_BEFORE_MAIL_MS, + 'last_sent_ts': self.get_room_last_sent_ts(push_action['room_id']), + 'throttle_ms': self.get_room_throttle_ms(push_action['room_id']), + } + + yield self.send_notification(unprocessed, reason) yield self.save_last_stream_ordering_and_success(max([ ea['stream_ordering'] for ea in unprocessed @@ -195,7 +209,8 @@ class EmailPusher(object): """ Determines whether throttling should prevent us from sending an email for the given room - Returns: True if we should send, False if we should not + Returns: The timestamp when we are next allowed to send an email notif + for this room """ last_sent_ts = self.get_room_last_sent_ts(room_id) throttle_ms = self.get_room_throttle_ms(room_id) @@ -244,8 +259,9 @@ class EmailPusher(object): ) @defer.inlineCallbacks - def send_notification(self, push_actions): + def send_notification(self, push_actions, reason): logger.info("Sending notif email for user %r", self.user_id) + yield self.mailer.send_notification_mail( - self.user_id, self.email, push_actions + self.user_id, self.email, push_actions, reason ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 2fd38a036a..c2c2ca3fa7 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -92,7 +92,7 @@ class Mailer(object): ) @defer.inlineCallbacks - def send_notification_mail(self, user_id, email_address, push_actions): + def send_notification_mail(self, user_id, email_address, push_actions, reason): raw_from = email.utils.parseaddr(self.hs.config.email_notif_from)[1] raw_to = email.utils.parseaddr(email_address)[1] @@ -143,12 +143,17 @@ class Mailer(object): notifs_by_room, state_by_room, notif_events, user_id ) + reason['room_name'] = calculate_room_name( + state_by_room[reason['room_id']], user_id, fallback_to_members=False + ) + template_vars = { "user_display_name": user_display_name, "unsubscribe_link": self.make_unsubscribe_link(), "summary_text": summary_text, "app_name": self.app_name, "rooms": rooms, + "reason": reason, } html_text = self.notif_template_html.render(**template_vars) -- cgit 1.5.1 From e837df6adb3a3b103b5fbc4f55e110af3a2fcc71 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 May 2016 11:53:25 +0100 Subject: increment badge count per missed convo, not per msg --- synapse/push/push_tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py index e71d01e77d..89a3b5e90a 100644 --- a/synapse/push/push_tools.py +++ b/synapse/push/push_tools.py @@ -38,7 +38,9 @@ def get_badge_count(store, user_id): r.room_id, user_id, last_unread_event_id ) ) - badge += notifs["notify_count"] + # return one badge count per conversation, as count per + # message is so noisy as to be almost useless + badge += 1 if notifs["notify_count"] else 0 defer.returnValue(badge) -- cgit 1.5.1 From 989bdc9e569e6ca414369e90e7b88e2f1d99c753 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 23 May 2016 19:24:11 +0100 Subject: Tune email notifs to make them quieter: * After initial 10 minute window, only alert every 24h for room notifs * Reset room state after 6h of idleness * Synchronise throttles for messages sent in the same notif, so the 24 hourly notifs 'line up' * Fix the email subjects to say what triggered the notification * Order the rooms in reverse activity order in the email, so the 'reason' room should always come first --- synapse/push/emailpusher.py | 26 ++++++++++++++++-------- synapse/push/mailer.py | 48 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 18 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index b4b728adc5..a72cba8306 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -32,12 +32,19 @@ DELAY_BEFORE_MAIL_MS = 10 * 60 * 1000 # Each room maintains its own throttle counter, but each new mail notification # sends the pending notifications for all rooms. THROTTLE_START_MS = 10 * 60 * 1000 -THROTTLE_MAX_MS = 24 * 60 * 60 * 1000 # (2 * 60 * 1000) * (2 ** 11) # ~3 days -THROTTLE_MULTIPLIER = 6 # 10 mins, 1 hour, 6 hours, 24 hours +THROTTLE_MAX_MS = 24 * 60 * 60 * 1000 # 24h +# THROTTLE_MULTIPLIER = 6 # 10 mins, 1 hour, 6 hours, 24 hours +THROTTLE_MULTIPLIER = 144 # 10 mins, 24 hours - i.e. jump straight to 1 day # If no event triggers a notification for this long after the previous, # the throttle is released. -THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days +# 12 hours - a gap of 12 hours in conversation is surely enough to merit a new +# notification when things get going again... +THROTTLE_RESET_AFTER_MS = (12 * 60 * 60 * 1000) + +# does each email include all unread notifs, or just the ones which have happened +# since the last mail? +INCLUDE_ALL_UNREAD_NOTIFS = True class EmailPusher(object): @@ -126,8 +133,9 @@ class EmailPusher(object): up logging, measures and guards against multiple instances of it being run. """ + start = 0 if INCLUDE_ALL_UNREAD_NOTIFS else self.last_stream_ordering unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( - self.user_id, self.last_stream_ordering, self.max_stream_ordering + self.user_id, start, self.max_stream_ordering ) soonest_due_at = None @@ -150,7 +158,6 @@ class EmailPusher(object): # we then consider all previously outstanding notifications # to be delivered. - # debugging: reason = { 'room_id': push_action['room_id'], 'now': self.clock.time_msec(), @@ -165,9 +172,12 @@ class EmailPusher(object): yield self.save_last_stream_ordering_and_success(max([ ea['stream_ordering'] for ea in unprocessed ])) - yield self.sent_notif_update_throttle( - push_action['room_id'], push_action - ) + + # we update the throttle on all the possible unprocessed push actions + for ea in unprocessed: + yield self.sent_notif_update_throttle( + ea['room_id'], ea + ) break else: if soonest_due_at is None or should_notify_at < soonest_due_at: diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index c2c2ca3fa7..8a9e9895ba 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -45,7 +45,10 @@ MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %(app)s from %(person)s " \ MESSAGE_FROM_PERSON = "You have a message on %(app)s from %(person)s..." MESSAGES_FROM_PERSON = "You have messages on %(app)s from %(person)s..." MESSAGES_IN_ROOM = "There are some messages on %(app)s for you in the %(room)s room..." -MESSAGES_IN_ROOMS = "Here are some messages on %(app)s you may have missed..." +MESSAGES_IN_ROOM_AND_OTHERS = \ + "You have messages on %(app)s in the %(room)s room and others..." +MESSAGES_FROM_PERSON_AND_OTHERS = \ + "You have messages on %(app)s from %(person)s and others..." INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the " \ "%(room)s room on %(app)s..." INVITE_FROM_PERSON = "%(person)s has invited you to chat on %(app)s..." @@ -128,9 +131,14 @@ class Mailer(object): state_by_room[room_id] = room_state # Run at most 3 of these at once: sync does 10 at a time but email - # notifs are much realtime than sync so we can afford to wait a bit. + # notifs are much less realtime than sync so we can afford to wait a bit. yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) + # actually sort our so-called rooms_in_order list, most recent room first + rooms_in_order = rooms_in_order.sort( + key=lambda r: -notifs_by_room[r]['received_ts'] + ) + rooms = [] for r in rooms_in_order: @@ -139,12 +147,12 @@ class Mailer(object): ) rooms.append(roomvars) - summary_text = self.make_summary_text( - notifs_by_room, state_by_room, notif_events, user_id + reason['room_name'] = calculate_room_name( + state_by_room[reason['room_id']], user_id, fallback_to_members=True ) - reason['room_name'] = calculate_room_name( - state_by_room[reason['room_id']], user_id, fallback_to_members=False + summary_text = self.make_summary_text( + notifs_by_room, state_by_room, notif_events, user_id, reason ) template_vars = { @@ -296,7 +304,8 @@ class Mailer(object): return messagevars - def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): + def make_summary_text(self, notifs_by_room, state_by_room, + notif_events, user_id, reason): if len(notifs_by_room) == 1: # Only one room has new stuff room_id = notifs_by_room.keys()[0] @@ -371,9 +380,28 @@ class Mailer(object): } else: # Stuff's happened in multiple different rooms - return MESSAGES_IN_ROOMS % { - "app": self.app_name, - } + + # ...but we still refer to the 'reason' room which triggered the mail + if reason['room_name'] is not None: + return MESSAGES_IN_ROOM_AND_OTHERS % { + "room": reason['room_name'], + "app": self.app_name, + } + else: + # If the reason room doesn't have a name, say who the messages + # are from explicitly to avoid, "messages in the Bob room" + sender_ids = list(set([ + notif_events[n['event_id']].sender + for n in notifs_by_room[reason['room_id']] + ])) + + return MESSAGES_FROM_PERSON_AND_OTHERS % { + "person": descriptor_from_member_events([ + state_by_room[reason['room_id']][("m.room.member", s)] + for s in sender_ids + ]), + "app": self.app_name, + } def make_room_link(self, room_id): # need /beta for Universal Links to work on iOS -- cgit 1.5.1 From 88ea5ab2c37345c1266db1446afa6d6472b7c9fe Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 23 May 2016 19:33:45 +0100 Subject: consistency is the better part of valour --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 8a9e9895ba..766888ca79 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -44,7 +44,7 @@ MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %(app)s from %(person)s " \ "in the %s room..." MESSAGE_FROM_PERSON = "You have a message on %(app)s from %(person)s..." MESSAGES_FROM_PERSON = "You have messages on %(app)s from %(person)s..." -MESSAGES_IN_ROOM = "There are some messages on %(app)s for you in the %(room)s room..." +MESSAGES_IN_ROOM = "You have messages on %(app)s in the %(room)s room..." MESSAGES_IN_ROOM_AND_OTHERS = \ "You have messages on %(app)s in the %(room)s room and others..." MESSAGES_FROM_PERSON_AND_OTHERS = \ -- cgit 1.5.1 From cb8a321bdd35278fc1214d2dd2c1a8163ce25871 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 23 May 2016 22:54:56 +0100 Subject: fix NPE in room ordering --- synapse/push/mailer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 766888ca79..d8a0c35c79 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -135,8 +135,8 @@ class Mailer(object): yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) # actually sort our so-called rooms_in_order list, most recent room first - rooms_in_order = rooms_in_order.sort( - key=lambda r: -notifs_by_room[r]['received_ts'] + rooms_in_order.sort( + key=lambda r: -(notifs_by_room[r][-1]['received_ts'] or 0) ) rooms = [] -- cgit 1.5.1 From b007ee46065ed25ed1ca248cf47c19ee7f4b56c2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 May 2016 15:11:28 +0100 Subject: Check for presence of 'avatar_url' key --- synapse/push/mailer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index d8a0c35c79..3ae92d1574 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -259,7 +259,9 @@ class Mailer(object): sender_state_event = room_state[("m.room.member", event.sender)] sender_name = name_from_member_event(sender_state_event) - sender_avatar_url = sender_state_event.content["avatar_url"] + sender_avatar_url = None + if "avatar_url" in sender_state_event.content: + sender_avatar_url = sender_state_event.content["avatar_url"] # 'hash' for deterministically picking default images: use # sender_hash % the number of default images to choose from -- cgit 1.5.1 From e5b0bbcd3381e581ecf9876760bbe36c66fcb4fe Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 31 May 2016 13:46:58 +0100 Subject: Add caches to bulk_get_push_rules* --- synapse/push/bulk_push_rule_evaluator.py | 8 +++++--- synapse/storage/push_rule.py | 16 ++++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 25e13b3423..25f2fb9da4 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -29,6 +29,7 @@ logger = logging.getLogger(__name__) def decode_rule_json(rule): + rule = dict(rule) rule['conditions'] = json.loads(rule['conditions']) rule['actions'] = json.loads(rule['actions']) return rule @@ -39,6 +40,8 @@ def _get_rules(room_id, user_ids, store): rules_by_user = yield store.bulk_get_push_rules(user_ids) rules_enabled_by_user = yield store.bulk_get_push_rules_enabled(user_ids) + rules_by_user = {k: v for k, v in rules_by_user.items() if v is not None} + rules_by_user = { uid: list_with_base_rules([ decode_rule_json(rule_list) @@ -51,11 +54,10 @@ def _get_rules(room_id, user_ids, store): # fetch disabled rules, but this won't account for any server default # rules the user has disabled, so we need to do this too. for uid in user_ids: - if uid not in rules_enabled_by_user: + user_enabled_map = rules_enabled_by_user.get(uid) + if not user_enabled_map: continue - user_enabled_map = rules_enabled_by_user[uid] - for i, rule in enumerate(rules_by_user[uid]): rule_id = rule['rule_id'] diff --git a/synapse/storage/push_rule.py b/synapse/storage/push_rule.py index d2bf7f2aec..f285f59afd 100644 --- a/synapse/storage/push_rule.py +++ b/synapse/storage/push_rule.py @@ -14,7 +14,7 @@ # limitations under the License. from ._base import SQLBaseStore -from synapse.util.caches.descriptors import cachedInlineCallbacks +from synapse.util.caches.descriptors import cachedInlineCallbacks, cachedList from twisted.internet import defer import logging @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) class PushRuleStore(SQLBaseStore): - @cachedInlineCallbacks() + @cachedInlineCallbacks(lru=True) def get_push_rules_for_user(self, user_id): rows = yield self._simple_select_list( table="push_rules", @@ -44,7 +44,7 @@ class PushRuleStore(SQLBaseStore): defer.returnValue(rows) - @cachedInlineCallbacks() + @cachedInlineCallbacks(lru=True) def get_push_rules_enabled_for_user(self, user_id): results = yield self._simple_select_list( table="push_rules_enable", @@ -60,7 +60,8 @@ class PushRuleStore(SQLBaseStore): r['rule_id']: False if r['enabled'] == 0 else True for r in results }) - @defer.inlineCallbacks + @cachedList(cached_method_name="get_push_rules_for_user", + list_name="user_ids", num_args=1, inlineCallbacks=True) def bulk_get_push_rules(self, user_ids): if not user_ids: defer.returnValue({}) @@ -75,13 +76,16 @@ class PushRuleStore(SQLBaseStore): desc="bulk_get_push_rules", ) - rows.sort(key=lambda e: (-e["priority_class"], -e["priority"])) + rows.sort( + key=lambda row: (-int(row["priority_class"]), -int(row["priority"])) + ) for row in rows: results.setdefault(row['user_name'], []).append(row) defer.returnValue(results) - @defer.inlineCallbacks + @cachedList(cached_method_name="get_push_rules_enabled_for_user", + list_name="user_ids", num_args=1, inlineCallbacks=True) def bulk_get_push_rules_enabled(self, user_ids): if not user_ids: defer.returnValue({}) -- cgit 1.5.1 From 43db0d9f6a314679bd25b82354e5c469e7a010b9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Jun 2016 10:31:09 +0100 Subject: Add get_users_with_read_receipts_in_room cache --- synapse/push/bulk_push_rule_evaluator.py | 8 ++++---- synapse/storage/receipts.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 25f2fb9da4..1e5c4b073c 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -87,13 +87,13 @@ def evaluator_for_event(event, hs, store): all_in_room = yield store.get_users_in_room(room_id) all_in_room = set(all_in_room) - receipts = yield store.get_receipts_for_room(room_id, "m.read") + users_with_receipts = yield store.get_users_with_read_receipts_in_room(room_id) # any users with pushers must be ours: they have pushers user_ids = set(users_with_pushers) - for r in receipts: - if hs.is_mine_id(r['user_id']) and r['user_id'] in all_in_room: - user_ids.add(r['user_id']) + for uid in users_with_receipts: + if hs.is_mine_id(uid) and uid in all_in_room: + user_ids.add(uid) # if this event is an invite event, we may need to run rules for the user # who's been invited, otherwise they won't get told they've been invited diff --git a/synapse/storage/receipts.py b/synapse/storage/receipts.py index fdcf28f3e1..964f30dff7 100644 --- a/synapse/storage/receipts.py +++ b/synapse/storage/receipts.py @@ -34,6 +34,26 @@ class ReceiptsStore(SQLBaseStore): "ReceiptsRoomChangeCache", self._receipts_id_gen.get_current_token() ) + @cachedInlineCallbacks() + def get_users_with_read_receipts_in_room(self, room_id): + receipts = yield self.get_receipts_for_room(room_id, "m.read") + defer.returnValue(set(r['user_id'] for r in receipts)) + + def _invalidate_get_users_with_receipts_in_room(self, room_id, receipt_type, + user_id): + if receipt_type != "m.read": + return + + # Returns an ObservableDeferred + res = self.get_users_with_read_receipts_in_room.cache.get((room_id,), None) + + if res and res.called and user_id in res.result: + # We'd only be adding to the set, so no point invalidating if the + # user is already there + return + + self.get_users_with_read_receipts_in_room.invalidate((room_id,)) + @cached(num_args=2) def get_receipts_for_room(self, room_id, receipt_type): return self._simple_select_list( @@ -228,6 +248,10 @@ class ReceiptsStore(SQLBaseStore): txn.call_after( self.get_receipts_for_room.invalidate, (room_id, receipt_type) ) + txn.call_after( + self._invalidate_get_users_with_receipts_in_room, + room_id, receipt_type, user_id, + ) txn.call_after( self.get_receipts_for_user.invalidate, (user_id, receipt_type) ) @@ -373,6 +397,10 @@ class ReceiptsStore(SQLBaseStore): txn.call_after( self.get_receipts_for_room.invalidate, (room_id, receipt_type) ) + txn.call_after( + self._invalidate_get_users_with_receipts_in_room, + room_id, receipt_type, user_id, + ) txn.call_after( self.get_receipts_for_user.invalidate, (user_id, receipt_type) ) -- cgit 1.5.1 From c8285564a3772db387fd5c94b1a82329dc320e36 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Jun 2016 11:08:45 +0100 Subject: Use state to calculate get_users_in_room --- synapse/push/action_generator.py | 2 +- synapse/push/bulk_push_rule_evaluator.py | 27 ++++++++++++++--------- synapse/storage/events.py | 3 --- synapse/storage/pusher.py | 38 +++++++++++++++++++++++--------- synapse/storage/roommember.py | 3 --- 5 files changed, 45 insertions(+), 28 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/action_generator.py b/synapse/push/action_generator.py index 9b208668b6..46e768e35c 100644 --- a/synapse/push/action_generator.py +++ b/synapse/push/action_generator.py @@ -40,7 +40,7 @@ class ActionGenerator: def handle_push_actions_for_event(self, event, context): with Measure(self.clock, "handle_push_actions_for_event"): bulk_evaluator = yield evaluator_for_event( - event, self.hs, self.store + event, self.hs, self.store, context.current_state ) actions_by_user = yield bulk_evaluator.action_for_event_by_user( diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 1e5c4b073c..8c59e59e03 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -21,7 +21,7 @@ from twisted.internet import defer from .baserules import list_with_base_rules from .push_rule_evaluator import PushRuleEvaluatorForEvent -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, Membership from synapse.visibility import filter_events_for_clients @@ -72,20 +72,24 @@ def _get_rules(room_id, user_ids, store): @defer.inlineCallbacks -def evaluator_for_event(event, hs, store): +def evaluator_for_event(event, hs, store, current_state): room_id = event.room_id - - # users in the room who have pushers need to get push rules run because - # that's how their pushers work - users_with_pushers = yield store.get_users_with_pushers_in_room(room_id) - # We also will want to generate notifs for other people in the room so # their unread countss are correct in the event stream, but to avoid # generating them for bot / AS users etc, we only do so for people who've # sent a read receipt into the room. - all_in_room = yield store.get_users_in_room(room_id) - all_in_room = set(all_in_room) + all_in_room = set( + e.state_key for e in current_state.values() + if e.type == EventTypes.Member and e.membership == Membership.JOIN + ) + + # users in the room who have pushers need to get push rules run because + # that's how their pushers work + if_users_with_pushers = yield store.get_if_users_have_pushers(all_in_room) + users_with_pushers = set( + uid for uid, have_pusher in if_users_with_pushers.items() if have_pusher + ) users_with_receipts = yield store.get_users_with_read_receipts_in_room(room_id) @@ -143,7 +147,10 @@ class BulkPushRuleEvaluator: self.store, user_tuples, [event], {event.event_id: current_state} ) - room_members = yield self.store.get_users_in_room(self.room_id) + room_members = set( + e.state_key for e in current_state.values() + if e.type == EventTypes.Member and e.membership == Membership.JOIN + ) evaluator = PushRuleEvaluatorForEvent(event, len(room_members)) diff --git a/synapse/storage/events.py b/synapse/storage/events.py index 4655669ba0..2b3f79577b 100644 --- a/synapse/storage/events.py +++ b/synapse/storage/events.py @@ -342,9 +342,6 @@ class EventsStore(SQLBaseStore): txn.call_after(self._get_current_state_for_key.invalidate_all) txn.call_after(self.get_rooms_for_user.invalidate_all) txn.call_after(self.get_users_in_room.invalidate, (event.room_id,)) - txn.call_after( - self.get_users_with_pushers_in_room.invalidate, (event.room_id,) - ) txn.call_after(self.get_joined_hosts_for_room.invalidate, (event.room_id,)) txn.call_after(self.get_room_name_and_aliases.invalidate, (event.room_id,)) diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index 9e8e2e2964..39d5349eaa 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -18,7 +18,7 @@ from twisted.internet import defer from canonicaljson import encode_canonical_json -from synapse.util.caches.descriptors import cachedInlineCallbacks +from synapse.util.caches.descriptors import cachedInlineCallbacks, cachedList import logging import simplejson as json @@ -135,19 +135,35 @@ class PusherStore(SQLBaseStore): "get_all_updated_pushers", get_all_updated_pushers_txn ) - @cachedInlineCallbacks(num_args=1) - def get_users_with_pushers_in_room(self, room_id): - users = yield self.get_users_in_room(room_id) - + @cachedInlineCallbacks(lru=True, num_args=1) + def get_if_user_has_pusher(self, user_id): result = yield self._simple_select_many_batch( + table='pushers', + keyvalues={ + 'user_name': 'user_id', + }, + retcol='user_name', + desc='get_if_user_has_pusher', + allow_none=True, + ) + + defer.returnValue(bool(result)) + + @cachedList(cached_method_name="get_if_user_has_pusher", + list_name="user_ids", num_args=1, inlineCallbacks=True) + def get_if_users_have_pushers(self, user_ids): + rows = yield self._simple_select_many_batch( table='pushers', column='user_name', - iterable=users, + iterable=user_ids, retcols=['user_name'], - desc='get_users_with_pushers_in_room' + desc='get_if_users_have_pushers' ) - defer.returnValue([r['user_name'] for r in result]) + result = {user_id: False for user_id in user_ids} + result.update({r['user_name']: True for r in rows}) + + defer.returnValue(result) @defer.inlineCallbacks def add_pusher(self, user_id, access_token, kind, app_id, @@ -178,16 +194,16 @@ class PusherStore(SQLBaseStore): }, ) if newly_inserted: - # get_users_with_pushers_in_room only cares if the user has + # get_if_user_has_pusher only cares if the user has # at least *one* pusher. - txn.call_after(self.get_users_with_pushers_in_room.invalidate_all) + txn.call_after(self.get_if_user_has_pusher.invalidate, (user_id,)) yield self.runInteraction("add_pusher", f) @defer.inlineCallbacks def delete_pusher_by_app_id_pushkey_user_id(self, app_id, pushkey, user_id): def delete_pusher_txn(txn, stream_id): - txn.call_after(self.get_users_with_pushers_in_room.invalidate_all) + txn.call_after(self.get_if_user_has_pusher.invalidate, (user_id,)) self._simple_delete_one_txn( txn, diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index face685ed2..41b395e07c 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -58,9 +58,6 @@ class RoomMemberStore(SQLBaseStore): txn.call_after(self.get_rooms_for_user.invalidate, (event.state_key,)) txn.call_after(self.get_joined_hosts_for_room.invalidate, (event.room_id,)) txn.call_after(self.get_users_in_room.invalidate, (event.room_id,)) - txn.call_after( - self.get_users_with_pushers_in_room.invalidate, (event.room_id,) - ) txn.call_after( self._membership_stream_cache.entity_has_changed, event.state_key, event.internal_metadata.stream_ordering -- cgit 1.5.1 From e793866398ffb3e222a86ebb15b9d24220accbc8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jun 2016 09:41:13 +0100 Subject: Use user_id in email greeting if display name is null --- synapse/push/mailer.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 3ae92d1574..fe5d67a03c 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -122,6 +122,8 @@ class Mailer(object): user_display_name = yield self.store.get_profile_displayname( UserID.from_string(user_id).localpart ) + if user_display_name is None: + user_display_name = user_id except StoreError: user_display_name = user_id -- cgit 1.5.1 From a15ad608496fd29fb8bf289152c23adca822beca Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jun 2016 11:44:15 +0100 Subject: Email unsubscribing that may in theory, work Were it not for that fact that you can't use the base handler in the pusher because it pulls in the world. Comitting while I fix that on a different branch. --- synapse/handlers/auth.py | 5 +++++ synapse/push/emailpusher.py | 2 +- synapse/push/mailer.py | 21 ++++++++++++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) (limited to 'synapse/push') diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 26c865e171..200793b5ed 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -529,6 +529,11 @@ class AuthHandler(BaseHandler): macaroon.add_first_party_caveat("time < %d" % (expiry,)) return macaroon.serialize() + def generate_delete_pusher_token(self, user_id): + macaroon = self._generate_base_macaroon(user_id) + macaroon.add_first_party_caveat("type = delete_pusher") + return macaroon.serialize() + def validate_short_term_login_token_and_get_user_id(self, login_token): try: macaroon = pymacaroons.Macaroon.deserialize(login_token) diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index a72cba8306..46d7c0434b 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -273,5 +273,5 @@ class EmailPusher(object): logger.info("Sending notif email for user %r", self.user_id) yield self.mailer.send_notification_mail( - self.user_id, self.email, push_actions, reason + self.app_id, self.user_id, self.email, push_actions, reason ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 3ae92d1574..95250bad7d 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -81,6 +81,7 @@ class Mailer(object): def __init__(self, hs): self.hs = hs self.store = self.hs.get_datastore() + self.handlers = self.hs.get_handlers() self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) self.app_name = self.hs.config.email_app_name @@ -95,7 +96,8 @@ class Mailer(object): ) @defer.inlineCallbacks - def send_notification_mail(self, user_id, email_address, push_actions, reason): + def send_notification_mail(self, app_id, user_id, email_address, + push_actions, reason): raw_from = email.utils.parseaddr(self.hs.config.email_notif_from)[1] raw_to = email.utils.parseaddr(email_address)[1] @@ -157,7 +159,7 @@ class Mailer(object): template_vars = { "user_display_name": user_display_name, - "unsubscribe_link": self.make_unsubscribe_link(), + "unsubscribe_link": self.make_unsubscribe_link(app_id, email_address), "summary_text": summary_text, "app_name": self.app_name, "rooms": rooms, @@ -423,9 +425,18 @@ class Mailer(object): notif['room_id'], notif['event_id'] ) - def make_unsubscribe_link(self): - # XXX: matrix.to - return "https://vector.im/#/settings" + def make_unsubscribe_link(self, app_id, email_address): + params = { + "access_token": self.handlers.auth.generate_delete_pusher_token(), + "app_id": app_id, + "pushkey": email_address, + } + + # XXX: make r0 once API is stable + return "%s_matrix/client/unstable/pushers/remove?%s" % ( + self.hs.config.public_baseurl, + urllib.urlencode(params), + ) def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": -- cgit 1.5.1 From f84b89f0c6b2e67897fec8639b79bf1d45c8f2b6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 2 Jun 2016 13:29:48 +0100 Subject: if an email pusher specifies a brand param, use it --- synapse/push/emailpusher.py | 7 ++++++- synapse/push/mailer.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index a72cba8306..e38ed02006 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -72,7 +72,12 @@ class EmailPusher(object): self.processing = False if self.hs.config.email_enable_notifs: - self.mailer = Mailer(self.hs) + if 'data' in pusherdict and 'brand' in pusherdict['data']: + app_name = pusherdict['data']['brand'] + else: + app_name = self.hs.config.email_app_name + + self.mailer = Mailer(self.hs, app_name) else: self.mailer = None diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index fe5d67a03c..0e9d8ccb53 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -78,12 +78,12 @@ ALLOWED_ATTRS = { class Mailer(object): - def __init__(self, hs): + def __init__(self, hs, app_name): self.hs = hs self.store = self.hs.get_datastore() self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) - self.app_name = self.hs.config.email_app_name + self.app_name = app_name env = jinja2.Environment(loader=loader) env.filters["format_ts"] = format_ts_filter env.filters["mxc_to_http"] = self.mxc_to_http_filter -- cgit 1.5.1 From 356f13c0696526032c211c103dad2f57d18473fa Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 2 Jun 2016 14:07:38 +0100 Subject: Disable INCLUDE_ALL_UNREAD_NOTIFS --- synapse/push/emailpusher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index e38ed02006..2c21ed3088 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -44,7 +44,8 @@ THROTTLE_RESET_AFTER_MS = (12 * 60 * 60 * 1000) # does each email include all unread notifs, or just the ones which have happened # since the last mail? -INCLUDE_ALL_UNREAD_NOTIFS = True +# XXX: this is currently broken as it includes ones from parted rooms(!) +INCLUDE_ALL_UNREAD_NOTIFS = False class EmailPusher(object): -- cgit 1.5.1 From 07a555991600137c830eb3b06f90a305c8f1e3d8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jun 2016 17:17:16 +0100 Subject: Fix error in email notification string formatting --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 0e9d8ccb53..63c7ec18a5 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -41,7 +41,7 @@ logger = logging.getLogger(__name__) MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %(app)s from %(person)s " \ - "in the %s room..." + "in the %(room)s room..." MESSAGE_FROM_PERSON = "You have a message on %(app)s from %(person)s..." MESSAGES_FROM_PERSON = "You have messages on %(app)s from %(person)s..." MESSAGES_IN_ROOM = "You have messages on %(app)s in the %(room)s room..." -- cgit 1.5.1 From 2675c1e40ebc3392ce719ac2304b97e98c7fefb4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 2 Jun 2016 17:21:12 +0100 Subject: add some branding debugging --- synapse/push/mailer.py | 1 + 1 file changed, 1 insertion(+) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 0e9d8ccb53..944f3b481d 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -84,6 +84,7 @@ class Mailer(object): self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) self.app_name = app_name + logger.info("Created Mailer for app_name %s" % app_name) env = jinja2.Environment(loader=loader) env.filters["format_ts"] = format_ts_filter env.filters["mxc_to_http"] = self.mxc_to_http_filter -- cgit 1.5.1 From 1f31cc37f8611f9ae5612ef5be82e63735fbdf34 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jun 2016 17:21:31 +0100 Subject: Working unsubscribe links going straight to the HS and authed by macaroons that let you delete pushers and nothing else --- synapse/api/auth.py | 7 +++++++ synapse/app/pusher.py | 23 ++++++++++++++++++++++- synapse/push/mailer.py | 8 ++++---- synapse/rest/client/v1/pusher.py | 4 +++- 4 files changed, 36 insertions(+), 6 deletions(-) (limited to 'synapse/push') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 463bd8b692..31e1abb964 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -660,6 +660,13 @@ class Auth(object): "is_guest": True, "token_id": None, } + elif rights == "delete_pusher": + # We don't store these tokens in the database + ret = { + "user": user, + "is_guest": False, + "token_id": None, + } else: # This codepath exists so that we can actually return a # token ID, because we use token IDs in place of device diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 135dd58c15..f1de1e7ce9 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -21,6 +21,7 @@ from synapse.config._base import ConfigError from synapse.config.database import DatabaseConfig from synapse.config.logger import LoggingConfig from synapse.config.emailconfig import EmailConfig +from synapse.config.key import KeyConfig from synapse.http.site import SynapseSite from synapse.metrics.resource import MetricsResource, METRICS_PREFIX from synapse.storage.roommember import RoomMemberStore @@ -63,6 +64,26 @@ class SlaveConfig(DatabaseConfig): self.pid_file = self.abspath(config.get("pid_file")) self.public_baseurl = config["public_baseurl"] + # some things used by the auth handler but not actually used in the + # pusher codebase + self.bcrypt_rounds = None + self.ldap_enabled = None + self.ldap_server = None + self.ldap_port = None + self.ldap_tls = None + self.ldap_search_base = None + self.ldap_search_property = None + self.ldap_email_property = None + self.ldap_full_name_property = None + + # We would otherwise try to use the registration shared secret as the + # macaroon shared secret if there was no macaroon_shared_secret, but + # that means pulling in RegistrationConfig too. We don't need to be + # backwards compaitible in the pusher codebase so just make people set + # macaroon_shared_secret. We set this to None to prevent it referencing + # an undefined key. + self.registration_shared_secret = None + def default_config(self, server_name, **kwargs): pid_file = self.abspath("pusher.pid") return """\ @@ -95,7 +116,7 @@ class SlaveConfig(DatabaseConfig): """ % locals() -class PusherSlaveConfig(SlaveConfig, LoggingConfig, EmailConfig): +class PusherSlaveConfig(SlaveConfig, LoggingConfig, EmailConfig, KeyConfig): pass diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e877d8fdad..60d3700afa 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -81,7 +81,7 @@ class Mailer(object): def __init__(self, hs, app_name): self.hs = hs self.store = self.hs.get_datastore() - self.handlers = self.hs.get_handlers() + self.auth_handler = self.hs.get_auth_handler() self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) self.app_name = app_name @@ -161,7 +161,7 @@ class Mailer(object): template_vars = { "user_display_name": user_display_name, - "unsubscribe_link": self.make_unsubscribe_link(app_id, email_address), + "unsubscribe_link": self.make_unsubscribe_link(user_id, app_id, email_address), "summary_text": summary_text, "app_name": self.app_name, "rooms": rooms, @@ -427,9 +427,9 @@ class Mailer(object): notif['room_id'], notif['event_id'] ) - def make_unsubscribe_link(self, app_id, email_address): + def make_unsubscribe_link(self, user_id, app_id, email_address): params = { - "access_token": self.handlers.auth.generate_delete_pusher_token(), + "access_token": self.auth_handler.generate_delete_pusher_token(user_id), "app_id": app_id, "pushkey": email_address, } diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index fa7a0992dd..9a2ed6ed88 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -149,11 +149,13 @@ class PushersRemoveRestServlet(RestServlet): def __init__(self, hs): super(RestServlet, self).__init__() + self.hs = hs self.notifier = hs.get_notifier() + self.auth = hs.get_v1auth() @defer.inlineCallbacks def on_GET(self, request): - requester = yield self.auth.get_user_by_req(request, "delete_pusher") + requester = yield self.auth.get_user_by_req(request, rights="delete_pusher") user = requester.user app_id = parse_string(request, "app_id", required=True) -- cgit 1.5.1 From 745ddb4dd04d0346f27f65a1e5508900b58e658a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jun 2016 17:38:41 +0100 Subject: peppate --- synapse/push/mailer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 60d3700afa..3c9a66008d 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -161,7 +161,9 @@ class Mailer(object): template_vars = { "user_display_name": user_display_name, - "unsubscribe_link": self.make_unsubscribe_link(user_id, app_id, email_address), + "unsubscribe_link": self.make_unsubscribe_link( + user_id, app_id, email_address + ), "summary_text": summary_text, "app_name": self.app_name, "rooms": rooms, -- cgit 1.5.1 From 79d1f072f4fc9a6b2e9773e8cb700b26bc2dff51 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 2 Jun 2016 21:34:40 +0100 Subject: brand the email from header --- synapse/config/emailconfig.py | 2 +- synapse/push/mailer.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'synapse/push') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 90bdd08f00..a187161272 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -89,7 +89,7 @@ class EmailConfig(Config): # enable_notifs: false # smtp_host: "localhost" # smtp_port: 25 - # notif_from: Your Friendly Matrix Home Server + # notif_from: "Your Friendly %(app)s Home Server " # app_name: Matrix # template_dir: res/templates # notif_template_html: notif_mail.html diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 944f3b481d..c1e9057eb6 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -97,7 +97,14 @@ class Mailer(object): @defer.inlineCallbacks def send_notification_mail(self, user_id, email_address, push_actions, reason): - raw_from = email.utils.parseaddr(self.hs.config.email_notif_from)[1] + try: + from_string = self.hs.config.email_notif_from % { + "app": self.app_name + } + except TypeError: + from_string = self.hs.config.email_notif_from + + raw_from = email.utils.parseaddr(from_string)[1] raw_to = email.utils.parseaddr(email_address)[1] if raw_to == '': -- cgit 1.5.1 From 9c26b390a2112590fe4252057dc1f081cb99a6b1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Jun 2016 11:34:06 +0100 Subject: Only get local users --- synapse/push/bulk_push_rule_evaluator.py | 7 +++++-- synapse/storage/pusher.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 8c59e59e03..d50db3b736 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -83,10 +83,13 @@ def evaluator_for_event(event, hs, store, current_state): e.state_key for e in current_state.values() if e.type == EventTypes.Member and e.membership == Membership.JOIN ) + local_users_in_room = set(uid for uid in all_in_room if hs.is_mine_id(uid)) # users in the room who have pushers need to get push rules run because # that's how their pushers work - if_users_with_pushers = yield store.get_if_users_have_pushers(all_in_room) + if_users_with_pushers = yield store.get_if_users_have_pushers( + local_users_in_room + ) users_with_pushers = set( uid for uid, have_pusher in if_users_with_pushers.items() if have_pusher ) @@ -96,7 +99,7 @@ def evaluator_for_event(event, hs, store, current_state): # any users with pushers must be ours: they have pushers user_ids = set(users_with_pushers) for uid in users_with_receipts: - if hs.is_mine_id(uid) and uid in all_in_room: + if uid in local_users_in_room: user_ids.add(uid) # if this event is an invite event, we may need to run rules for the user diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index 39d5349eaa..a7d7c54d7e 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -135,7 +135,7 @@ class PusherStore(SQLBaseStore): "get_all_updated_pushers", get_all_updated_pushers_txn ) - @cachedInlineCallbacks(lru=True, num_args=1) + @cachedInlineCallbacks(lru=True, num_args=1, max_entries=15000) def get_if_user_has_pusher(self, user_id): result = yield self._simple_select_many_batch( table='pushers', -- cgit 1.5.1 From 59f2d7352224af97e8f091f673858dde42b00197 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Jun 2016 15:45:37 +0100 Subject: Remove unnecessary sets --- synapse/push/bulk_push_rule_evaluator.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index d50db3b736..af5212a5d1 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -79,25 +79,24 @@ def evaluator_for_event(event, hs, store, current_state): # generating them for bot / AS users etc, we only do so for people who've # sent a read receipt into the room. - all_in_room = set( + local_users_in_room = set( e.state_key for e in current_state.values() if e.type == EventTypes.Member and e.membership == Membership.JOIN + and hs.is_mine_id(e.state_key) ) - local_users_in_room = set(uid for uid in all_in_room if hs.is_mine_id(uid)) # users in the room who have pushers need to get push rules run because # that's how their pushers work if_users_with_pushers = yield store.get_if_users_have_pushers( local_users_in_room ) - users_with_pushers = set( + user_ids = set( uid for uid, have_pusher in if_users_with_pushers.items() if have_pusher ) users_with_receipts = yield store.get_users_with_read_receipts_in_room(room_id) # any users with pushers must be ours: they have pushers - user_ids = set(users_with_pushers) for uid in users_with_receipts: if uid in local_users_in_room: user_ids.add(uid) @@ -111,8 +110,6 @@ def evaluator_for_event(event, hs, store, current_state): if has_pusher: user_ids.add(invited_user) - user_ids = list(user_ids) - rules_by_user = yield _get_rules(room_id, user_ids, store) defer.returnValue(BulkPushRuleEvaluator( -- cgit 1.5.1 From 6a0afa582aa5bf816e082af31ac44e2a8fee28c0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Jun 2016 14:27:07 +0100 Subject: Load push rules in storage layer, so that they get cached --- synapse/handlers/sync.py | 5 ++--- synapse/push/bulk_push_rule_evaluator.py | 28 ----------------------- synapse/push/clientformat.py | 30 ++++++++++++++++++------- synapse/rest/client/v1/push_rule.py | 6 ++--- synapse/storage/push_rule.py | 38 +++++++++++++++++++++++++++++++- 5 files changed, 63 insertions(+), 44 deletions(-) (limited to 'synapse/push') diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 5307b62b85..be26a491ff 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -198,9 +198,8 @@ class SyncHandler(object): @defer.inlineCallbacks def push_rules_for_user(self, user): user_id = user.to_string() - rawrules = yield self.store.get_push_rules_for_user(user_id) - enabled_map = yield self.store.get_push_rules_enabled_for_user(user_id) - rules = format_push_rules_for_user(user, rawrules, enabled_map) + rules = yield self.store.get_push_rules_for_user(user_id) + rules = format_push_rules_for_user(user, rules) defer.returnValue(rules) @defer.inlineCallbacks diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index af5212a5d1..6e42121b1d 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -18,7 +18,6 @@ import ujson as json from twisted.internet import defer -from .baserules import list_with_base_rules from .push_rule_evaluator import PushRuleEvaluatorForEvent from synapse.api.constants import EventTypes, Membership @@ -38,36 +37,9 @@ def decode_rule_json(rule): @defer.inlineCallbacks def _get_rules(room_id, user_ids, store): rules_by_user = yield store.bulk_get_push_rules(user_ids) - rules_enabled_by_user = yield store.bulk_get_push_rules_enabled(user_ids) rules_by_user = {k: v for k, v in rules_by_user.items() if v is not None} - rules_by_user = { - uid: list_with_base_rules([ - decode_rule_json(rule_list) - for rule_list in rules_by_user.get(uid, []) - ]) - for uid in user_ids - } - - # We apply the rules-enabled map here: bulk_get_push_rules doesn't - # fetch disabled rules, but this won't account for any server default - # rules the user has disabled, so we need to do this too. - for uid in user_ids: - user_enabled_map = rules_enabled_by_user.get(uid) - if not user_enabled_map: - continue - - for i, rule in enumerate(rules_by_user[uid]): - rule_id = rule['rule_id'] - - if rule_id in user_enabled_map: - if rule.get('enabled', True) != bool(user_enabled_map[rule_id]): - # Rules are cached across users. - rule = dict(rule) - rule['enabled'] = bool(user_enabled_map[rule_id]) - rules_by_user[uid][i] = rule - defer.returnValue(rules_by_user) diff --git a/synapse/push/clientformat.py b/synapse/push/clientformat.py index ae9db9ec2f..b3983f7940 100644 --- a/synapse/push/clientformat.py +++ b/synapse/push/clientformat.py @@ -23,10 +23,7 @@ import copy import simplejson as json -def format_push_rules_for_user(user, rawrules, enabled_map): - """Converts a list of rawrules and a enabled map into nested dictionaries - to match the Matrix client-server format for push rules""" - +def load_rules_for_user(user, rawrules, enabled_map): ruleslist = [] for rawrule in rawrules: rule = dict(rawrule) @@ -35,7 +32,26 @@ def format_push_rules_for_user(user, rawrules, enabled_map): ruleslist.append(rule) # We're going to be mutating this a lot, so do a deep copy - ruleslist = copy.deepcopy(list_with_base_rules(ruleslist)) + rules = list(list_with_base_rules(ruleslist)) + + for i, rule in enumerate(rules): + rule_id = rule['rule_id'] + if rule_id in enabled_map: + if rule.get('enabled', True) != bool(enabled_map[rule_id]): + # Rules are cached across users. + rule = dict(rule) + rule['enabled'] = bool(enabled_map[rule_id]) + rules[i] = rule + + return rules + + +def format_push_rules_for_user(user, ruleslist): + """Converts a list of rawrules and a enabled map into nested dictionaries + to match the Matrix client-server format for push rules""" + + # We're going to be mutating this a lot, so do a deep copy + ruleslist = copy.deepcopy(ruleslist) rules = {'global': {}, 'device': {}} @@ -60,9 +76,7 @@ def format_push_rules_for_user(user, rawrules, enabled_map): template_rule = _rule_to_template(r) if template_rule: - if r['rule_id'] in enabled_map: - template_rule['enabled'] = enabled_map[r['rule_id']] - elif 'enabled' in r: + if 'enabled' in r: template_rule['enabled'] = r['enabled'] else: template_rule['enabled'] = True diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py index 02d837ee6a..6bb4821ec6 100644 --- a/synapse/rest/client/v1/push_rule.py +++ b/synapse/rest/client/v1/push_rule.py @@ -128,11 +128,9 @@ class PushRuleRestServlet(ClientV1RestServlet): # we build up the full structure and then decide which bits of it # to send which means doing unnecessary work sometimes but is # is probably not going to make a whole lot of difference - rawrules = yield self.store.get_push_rules_for_user(user_id) + rules = yield self.store.get_push_rules_for_user(user_id) - enabled_map = yield self.store.get_push_rules_enabled_for_user(user_id) - - rules = format_push_rules_for_user(requester.user, rawrules, enabled_map) + rules = format_push_rules_for_user(requester.user, rules) path = request.postpath[1:] diff --git a/synapse/storage/push_rule.py b/synapse/storage/push_rule.py index ebb97c8474..786d6f6d67 100644 --- a/synapse/storage/push_rule.py +++ b/synapse/storage/push_rule.py @@ -15,6 +15,7 @@ from ._base import SQLBaseStore from synapse.util.caches.descriptors import cachedInlineCallbacks, cachedList +from synapse.push.baserules import list_with_base_rules from twisted.internet import defer import logging @@ -23,6 +24,29 @@ import simplejson as json logger = logging.getLogger(__name__) +def _load_rules(rawrules, enabled_map): + ruleslist = [] + for rawrule in rawrules: + rule = dict(rawrule) + rule["conditions"] = json.loads(rawrule["conditions"]) + rule["actions"] = json.loads(rawrule["actions"]) + ruleslist.append(rule) + + # We're going to be mutating this a lot, so do a deep copy + rules = list(list_with_base_rules(ruleslist)) + + for i, rule in enumerate(rules): + rule_id = rule['rule_id'] + if rule_id in enabled_map: + if rule.get('enabled', True) != bool(enabled_map[rule_id]): + # Rules are cached across users. + rule = dict(rule) + rule['enabled'] = bool(enabled_map[rule_id]) + rules[i] = rule + + return rules + + class PushRuleStore(SQLBaseStore): @cachedInlineCallbacks(lru=True) def get_push_rules_for_user(self, user_id): @@ -42,7 +66,11 @@ class PushRuleStore(SQLBaseStore): key=lambda row: (-int(row["priority_class"]), -int(row["priority"])) ) - defer.returnValue(rows) + enabled_map = yield self.get_push_rules_enabled_for_user(user_id) + + rules = _load_rules(rows, enabled_map) + + defer.returnValue(rules) @cachedInlineCallbacks(lru=True) def get_push_rules_enabled_for_user(self, user_id): @@ -85,6 +113,14 @@ class PushRuleStore(SQLBaseStore): for row in rows: results.setdefault(row['user_name'], []).append(row) + + enabled_map_by_user = yield self.bulk_get_push_rules_enabled(user_ids) + + for user_id, rules in results.items(): + results[user_id] = _load_rules( + rules, enabled_map_by_user.get(user_id, {}) + ) + defer.returnValue(results) @cachedList(cached_method_name="get_push_rules_enabled_for_user", -- cgit 1.5.1 From 06d40c8b9841cd877e70e205d55a08f423ff2ec9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jun 2016 16:31:23 +0100 Subject: Add substitutions to email notif From --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 88402e42a6..933a53fc3e 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -186,7 +186,7 @@ class Mailer(object): multipart_msg = MIMEMultipart('alternative') multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text) - multipart_msg['From'] = self.hs.config.email_notif_from + multipart_msg['From'] = self.hs.config.email_notif_from % (self.app_name, ) multipart_msg['To'] = email_address multipart_msg['Date'] = email.utils.formatdate() multipart_msg['Message-ID'] = email.utils.make_msgid() -- cgit 1.5.1 From fbf608decbf85051379dc24446b1b6e89ff97e8c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jun 2016 16:38:39 +0100 Subject: Oops, we're using the dict form --- synapse/push/mailer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 933a53fc3e..011bc4d2b1 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -186,7 +186,9 @@ class Mailer(object): multipart_msg = MIMEMultipart('alternative') multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text) - multipart_msg['From'] = self.hs.config.email_notif_from % (self.app_name, ) + multipart_msg['From'] = self.hs.config.email_notif_from % { + "app": self.app_name + } multipart_msg['To'] = email_address multipart_msg['Date'] = email.utils.formatdate() multipart_msg['Message-ID'] = email.utils.make_msgid() -- cgit 1.5.1 From 72c4d482e99d30fe96e2b24389629abe5b572626 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jun 2016 16:39:50 +0100 Subject: 3rd time lucky: we'd already calculated it above --- synapse/push/mailer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'synapse/push') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 011bc4d2b1..e5c3929cd7 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -186,9 +186,7 @@ class Mailer(object): multipart_msg = MIMEMultipart('alternative') multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text) - multipart_msg['From'] = self.hs.config.email_notif_from % { - "app": self.app_name - } + multipart_msg['From'] = from_string multipart_msg['To'] = email_address multipart_msg['Date'] = email.utils.formatdate() multipart_msg['Message-ID'] = email.utils.make_msgid() -- cgit 1.5.1 From 0b2158719c43eab87ab7a9448ae1d85008b92b92 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 7 Jun 2016 15:07:11 +0100 Subject: Remove dead code. Loading push rules now happens in the datastore, so we can remove the methods that loaded them outside the datastore. The ``waiting_for_join_list`` in federation handler is populated by anything, so can be removed. The ``_get_members_events_txn`` method isn't called from anywhere so can be removed. --- synapse/handlers/federation.py | 13 ------------- synapse/push/bulk_push_rule_evaluator.py | 8 -------- synapse/push/clientformat.py | 26 -------------------------- synapse/storage/roommember.py | 7 ------- 4 files changed, 54 deletions(-) (limited to 'synapse/push') diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 648a505e65..ff83c608e7 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -66,10 +66,6 @@ class FederationHandler(BaseHandler): self.hs = hs - self.distributor.observe("user_joined_room", self.user_joined_room) - - self.waiting_for_join_list = {} - self.store = hs.get_datastore() self.replication_layer = hs.get_replication_layer() self.state_handler = hs.get_state_handler() @@ -1091,15 +1087,6 @@ class FederationHandler(BaseHandler): def get_min_depth_for_context(self, context): return self.store.get_min_depth(context) - @log_function - def user_joined_room(self, user, room_id): - waiters = self.waiting_for_join_list.get( - (user.to_string(), room_id), - [] - ) - while waiters: - waiters.pop().callback(None) - @defer.inlineCallbacks @log_function def _handle_new_event(self, origin, event, state=None, auth_events=None, diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 6e42121b1d..756e5da513 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -14,7 +14,6 @@ # limitations under the License. import logging -import ujson as json from twisted.internet import defer @@ -27,13 +26,6 @@ from synapse.visibility import filter_events_for_clients logger = logging.getLogger(__name__) -def decode_rule_json(rule): - rule = dict(rule) - rule['conditions'] = json.loads(rule['conditions']) - rule['actions'] = json.loads(rule['actions']) - return rule - - @defer.inlineCallbacks def _get_rules(room_id, user_ids, store): rules_by_user = yield store.bulk_get_push_rules(user_ids) diff --git a/synapse/push/clientformat.py b/synapse/push/clientformat.py index b3983f7940..e0331b2d2d 100644 --- a/synapse/push/clientformat.py +++ b/synapse/push/clientformat.py @@ -13,37 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.push.baserules import list_with_base_rules - from synapse.push.rulekinds import ( PRIORITY_CLASS_MAP, PRIORITY_CLASS_INVERSE_MAP ) import copy -import simplejson as json - - -def load_rules_for_user(user, rawrules, enabled_map): - ruleslist = [] - for rawrule in rawrules: - rule = dict(rawrule) - rule["conditions"] = json.loads(rawrule["conditions"]) - rule["actions"] = json.loads(rawrule["actions"]) - ruleslist.append(rule) - - # We're going to be mutating this a lot, so do a deep copy - rules = list(list_with_base_rules(ruleslist)) - - for i, rule in enumerate(rules): - rule_id = rule['rule_id'] - if rule_id in enabled_map: - if rule.get('enabled', True) != bool(enabled_map[rule_id]): - # Rules are cached across users. - rule = dict(rule) - rule['enabled'] = bool(enabled_map[rule_id]) - rules[i] = rule - - return rules def format_push_rules_for_user(user, ruleslist): diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index 64b4bd371b..8bd693be72 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -243,13 +243,6 @@ class RoomMemberStore(SQLBaseStore): user_ids = yield self.get_users_in_room(room_id) defer.returnValue(set(get_domain_from_id(uid) for uid in user_ids)) - def _get_members_events_txn(self, txn, room_id, membership=None, user_id=None): - rows = self._get_members_rows_txn( - txn, - room_id, membership, user_id, - ) - return [r["event_id"] for r in rows] - def _get_members_rows_txn(self, txn, room_id, membership=None, user_id=None): where_clause = "c.room_id = ?" where_values = [room_id] -- cgit 1.5.1