From 19d6b6cd7aa32f844362f77ca7f0403af37fc396 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Apr 2016 19:22:53 +0100 Subject: Add WIP email template files --- res/templates/notif.html | 14 ++++++++++++++ res/templates/room.html | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 res/templates/notif.html create mode 100644 res/templates/room.html (limited to 'res/templates') diff --git a/res/templates/notif.html b/res/templates/notif.html new file mode 100644 index 0000000000..648ff034b3 --- /dev/null +++ b/res/templates/notif.html @@ -0,0 +1,14 @@ + + + +

{{ summaryText }}

+
+ {% for room in rooms %} + {% include 'room.html' with context %} + {% endfor %} +
+ + + diff --git a/res/templates/room.html b/res/templates/room.html new file mode 100644 index 0000000000..ef36b4ee58 --- /dev/null +++ b/res/templates/room.html @@ -0,0 +1,6 @@ +
+

{{ room.title }}

+
+ Things have happened in this room +
+
-- cgit 1.4.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 'res/templates') 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.4.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 'res/templates') 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.4.1 From 5367708236934a2b191991f1e861ec0d86c02be4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 10:55:32 +0100 Subject: Add the jinja template for individual notifs --- res/templates/notif.html | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 res/templates/notif.html (limited to 'res/templates') diff --git a/res/templates/notif.html b/res/templates/notif.html new file mode 100644 index 0000000000..aa6ed1e061 --- /dev/null +++ b/res/templates/notif.html @@ -0,0 +1,32 @@ +
+
+ {% 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.format == "org.matrix.custom.html" %} + {{ message.body_text_html }} + {% else %} + {{ message.body_text_plain }} + {% endif %} +
+
+ {% endfor %} +
+ +
-- cgit 1.4.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 'res/templates') 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.4.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 'res/templates') 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.4.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 'res/templates') 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.4.1 From 36bb5c23831444f35c018923be8d8c0b0562b331 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 17:28:48 +0100 Subject: Fix notification link --- res/templates/notif.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'res/templates') diff --git a/res/templates/notif.html b/res/templates/notif.html index 70f5655352..3112df9704 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -29,6 +29,6 @@ {% endfor %} -- cgit 1.4.1 From 46b5547a4227ec29e0b71e2138a2ebade39724ca Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 17:29:39 +0100 Subject: Some basic css to make it halfway legible --- res/templates/mail.css | 35 +++++++++++++++++++++++++++++++++++ res/templates/notif_mail.html | 25 ++++++++++++++++--------- res/templates/room.html | 14 +++++++------- 3 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 res/templates/mail.css (limited to 'res/templates') diff --git a/res/templates/mail.css b/res/templates/mail.css new file mode 100644 index 0000000000..be63b8c337 --- /dev/null +++ b/res/templates/mail.css @@ -0,0 +1,35 @@ +body { + font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size: 12pt; + margin: 1em; +} + +#page { + margin: auto; + width: 640px; + padding: 1em; + border: 1px solid #76cfa6; + border-radius: 5px; +} + +.room_avatar { + width: 48px; + height: 48px; + float: left; +} + +.room_content { + clear: left; +} + +.historical { + color: #888; +} + +.sender_name { + display: inline; +} + +.message_time { + display: inline; +} diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html index fbfb0a767c..cc3573e65a 100644 --- a/res/templates/notif_mail.html +++ b/res/templates/notif_mail.html @@ -1,15 +1,22 @@ + + + -
Hi {{ user_display_name }},
-
{{ summary_text }}
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
-