mirror of https://github.com/zulip/zulip.git
Parse @-mentions in bugdown and style them.
* This makes bugdown.convert take a `message` parameter. Properties for parsed mentions are added to the message object by the `Pattern` for use in do_send_messages. * Refactor repeated markdown rendering code into `Message` model methods. (imported from commit 4f0ed5570104c0210f984b6de21e9048e2b53fa0)
This commit is contained in:
parent
7604534050
commit
dce1f7f729
|
@ -192,42 +192,6 @@ def log_message(message):
|
|||
if not message.sending_client.name.startswith("test:"):
|
||||
log_event(message.to_log_dict())
|
||||
|
||||
# Match multi-word string between @** ** or match any one-word
|
||||
# sequences after @
|
||||
find_mentions_re = re.compile(r'\B@(?:\*\*([^\*]+)\*\*)|@(\w+)')
|
||||
|
||||
def mentioned_in_message(message):
|
||||
# Determine what, if any, users are mentioned with an @-notification
|
||||
# in this message
|
||||
#
|
||||
# TODO(leo) There is a minor regression in that we no longer
|
||||
# match just-first-names or just-last-names
|
||||
wildcards = ['all', 'everyone']
|
||||
|
||||
potential_mentions = find_mentions_re.findall(message.content)
|
||||
# Either the first or the second group matched something, take the
|
||||
# one that did (find_all returns a list with each item containing all groups)
|
||||
potential_mentions = map(lambda elem: elem[0] or elem[1], potential_mentions)
|
||||
|
||||
users = set()
|
||||
for mention in potential_mentions:
|
||||
if mention in wildcards:
|
||||
return (True, set())
|
||||
|
||||
attempts = [Q(full_name__iexact=mention), Q(short_name__iexact=mention)]
|
||||
found = False
|
||||
for attempt in attempts:
|
||||
ups = UserProfile.objects.filter(attempt, realm=message.sender.realm)
|
||||
for user in ups:
|
||||
users.add(user.id)
|
||||
found = len(ups) > 0
|
||||
break
|
||||
|
||||
if found:
|
||||
continue
|
||||
|
||||
return (False, users)
|
||||
|
||||
# Helper function. Defaults here are overriden by those set in do_send_messages
|
||||
def do_send_message(message, rendered_content = None, no_log = False, stream = None):
|
||||
do_send_messages([{'message': message,
|
||||
|
@ -270,6 +234,8 @@ def do_send_messages(messages):
|
|||
else:
|
||||
raise ValueError('Bad recipient type')
|
||||
|
||||
message['message'].maybe_render_content()
|
||||
|
||||
# Save the message receipts in the database
|
||||
user_message_flags = defaultdict(dict)
|
||||
with transaction.commit_on_success():
|
||||
|
@ -279,7 +245,12 @@ def do_send_messages(messages):
|
|||
ums_to_create = [UserMessage(user_profile=user_profile, message=message['message'])
|
||||
for user_profile in message['recipients']
|
||||
if user_profile.is_active]
|
||||
wildcard, mentioned_ids = mentioned_in_message(message['message'])
|
||||
|
||||
# These properties on the Message are set via
|
||||
# Message.render_markdown by code in the bugdown inline patterns
|
||||
wildcard = message['message'].mentions_wildcard
|
||||
mentioned_ids = message['message'].mentions_user_ids
|
||||
|
||||
for um in ums_to_create:
|
||||
sent_by_human = message['message'].sending_client.name.lower() in \
|
||||
['website', 'iphone', 'android']
|
||||
|
@ -298,7 +269,7 @@ def do_send_messages(messages):
|
|||
# Render Markdown etc. here and store (automatically) in
|
||||
# memcached, so that the single-threaded Tornado server
|
||||
# doesn't have to.
|
||||
message['message'].to_dict(apply_markdown=True, rendered_content=message['rendered_content'])
|
||||
message['message'].to_dict(apply_markdown=True)
|
||||
message['message'].to_dict(apply_markdown=False)
|
||||
user_flags = user_message_flags.get(message['message'].id, {})
|
||||
data = dict(
|
||||
|
@ -453,15 +424,9 @@ def check_message(sender, client, message_type_name, message_to,
|
|||
else:
|
||||
return "Invalid message type"
|
||||
|
||||
rendered_content = bugdown.convert(message_content, sender.realm.domain)
|
||||
if rendered_content is None:
|
||||
return "We were unable to render your message"
|
||||
|
||||
message = Message()
|
||||
message.sender = sender
|
||||
message.content = message_content
|
||||
message.rendered_content = rendered_content
|
||||
message.rendered_content_version = bugdown.version
|
||||
message.recipient = recipient
|
||||
if message_type_name == 'stream':
|
||||
message.subject = subject
|
||||
|
@ -472,11 +437,13 @@ def check_message(sender, client, message_type_name, message_to,
|
|||
message.pub_date = timezone.now()
|
||||
message.sending_client = client
|
||||
|
||||
if not message.maybe_render_content():
|
||||
return "We were unable to render your message"
|
||||
|
||||
if client.name == "zephyr_mirror" and already_sent_mirrored_message(message):
|
||||
return {'message': None}
|
||||
|
||||
return {'message': message, 'rendered_content': rendered_content,
|
||||
'stream': stream}
|
||||
return {'message': message, 'stream': stream}
|
||||
|
||||
def internal_prep_message(sender_email, recipient_type_name, recipients,
|
||||
subject, content, realm=None):
|
||||
|
@ -967,8 +934,8 @@ def do_update_message(user_profile, message_id, subject, content):
|
|||
if content is not None:
|
||||
if len(content) > MAX_MESSAGE_LENGTH:
|
||||
raise JsonableError("Message too long")
|
||||
rendered_content = bugdown.convert(content, message.sender.realm.domain)
|
||||
if rendered_content is None:
|
||||
rendered_content = message.render_markdown(content)
|
||||
if not rendered_content:
|
||||
raise JsonableError("We were unable to render your updated message")
|
||||
|
||||
if not settings.DEPLOYED or settings.STAGING_DEPLOYED:
|
||||
|
@ -981,8 +948,7 @@ def do_update_message(user_profile, message_id, subject, content):
|
|||
edit_history_event["prev_rendered_content"] = message.rendered_content
|
||||
edit_history_event["prev_rendered_content_version"] = message.rendered_content_version
|
||||
message.content = content
|
||||
message.rendered_content = rendered_content
|
||||
message.rendered_content_version = bugdown.version
|
||||
message.set_rendered_content(rendered_content)
|
||||
event["content"] = content
|
||||
event["rendered_content"] = rendered_content
|
||||
|
||||
|
@ -1014,8 +980,7 @@ def do_update_message(user_profile, message_id, subject, content):
|
|||
cache_save_message(message)
|
||||
items_for_memcached = {}
|
||||
items_for_memcached[to_dict_cache_key(message, True)] = \
|
||||
(stringify_message_dict(message.to_dict_uncached(apply_markdown=True,
|
||||
rendered_content=message.rendered_content)),)
|
||||
(stringify_message_dict(message.to_dict_uncached(apply_markdown=True)),)
|
||||
items_for_memcached[to_dict_cache_key(message, False)] = \
|
||||
(stringify_message_dict(message.to_dict_uncached(apply_markdown=False)),)
|
||||
cache_set_many(items_for_memcached)
|
||||
|
|
|
@ -21,6 +21,7 @@ from zephyr.lib.bugdown import codehilite, fenced_code
|
|||
from zephyr.lib.bugdown.fenced_code import FENCE_RE
|
||||
from zephyr.lib.timeout import timeout, TimeoutExpired
|
||||
from zephyr.lib.cache import cache_with_key, cache_get_many, cache_set_many
|
||||
import zephyr.lib.mention as mention
|
||||
|
||||
|
||||
if settings.USING_EMBEDLY:
|
||||
|
@ -517,6 +518,30 @@ class RealmFilterPattern(markdown.inlinepatterns.Pattern):
|
|||
return url_to_a(self.format_string % m.groupdict(),
|
||||
m.group("name"))
|
||||
|
||||
class UserMentionPattern(markdown.inlinepatterns.Pattern):
|
||||
def handleMatch(self, m):
|
||||
name = m.group(2) or m.group(3)
|
||||
|
||||
if current_message:
|
||||
wildcard, user = mention.find_user_for_mention(name, current_message.sender.realm)
|
||||
|
||||
if wildcard:
|
||||
current_message.mentions_wildcard = True
|
||||
email = "*"
|
||||
elif user:
|
||||
current_message.mentions_user_ids.add(user.id)
|
||||
name = user.full_name
|
||||
email = user.email
|
||||
else:
|
||||
# Don't highlight @mentions that don't refer to a valid user
|
||||
return None
|
||||
|
||||
el = markdown.util.etree.Element("span")
|
||||
el.set('class', 'user-mention')
|
||||
el.set('data-user-email', email)
|
||||
el.text = "@%s" % (name,)
|
||||
return el
|
||||
|
||||
class Bugdown(markdown.Extension):
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
del md.preprocessors['reference']
|
||||
|
@ -538,6 +563,7 @@ class Bugdown(markdown.Extension):
|
|||
md.parser.blockprocessors.add('ulist', UListProcessor(md.parser), '>hr')
|
||||
|
||||
md.inlinePatterns.add('gravatar', Gravatar(r'!gravatar\((?P<email>[^)]*)\)'), '_begin')
|
||||
md.inlinePatterns.add('usermention', UserMentionPattern(mention.find_mentions), '>backtick')
|
||||
md.inlinePatterns.add('emoji', Emoji(r'(?<!\S)(?P<syntax>:[^:\s]+:)(?!\S)'), '_begin')
|
||||
md.inlinePatterns.add('link', LinkPattern(markdown.inlinepatterns.LINK_RE, md), '>backtick')
|
||||
|
||||
|
@ -615,16 +641,23 @@ _privacy_re = re.compile(r'\w', flags=re.UNICODE)
|
|||
def _sanitize_for_log(md):
|
||||
return repr(_privacy_re.sub('x', md))
|
||||
|
||||
def do_convert(md, realm):
|
||||
|
||||
# Filters such as UserMentionPattern need a message, but python-markdown
|
||||
# provides no way to pass extra params through to a pattern. Thus, a global.
|
||||
current_message = None
|
||||
|
||||
def do_convert(md, realm_domain=None, message=None):
|
||||
"""Convert Markdown to HTML, with Humbug-specific settings and hacks."""
|
||||
|
||||
if realm in md_engines:
|
||||
_md_engine = md_engines[realm]
|
||||
if realm_domain in md_engines:
|
||||
_md_engine = md_engines[realm_domain]
|
||||
else:
|
||||
_md_engine = md_engines["default"]
|
||||
# Reset the parser; otherwise it will get slower over time.
|
||||
_md_engine.reset()
|
||||
|
||||
global current_message
|
||||
current_message = message
|
||||
try:
|
||||
# Spend at most 5 seconds rendering.
|
||||
# Sometimes Python-Markdown is really slow; see
|
||||
|
@ -645,7 +678,8 @@ def do_convert(md, realm):
|
|||
cleaned, traceback.format_exc()),
|
||||
fail_silently=False)
|
||||
return None
|
||||
|
||||
finally:
|
||||
current_message = None
|
||||
|
||||
bugdown_time_start = 0
|
||||
bugdown_total_time = 0
|
||||
|
@ -668,8 +702,8 @@ def bugdown_stats_finish():
|
|||
bugdown_total_requests += 1
|
||||
bugdown_total_time += (time.time() - bugdown_time_start)
|
||||
|
||||
def convert(md, realm):
|
||||
def convert(md, realm_domain=None, message=None):
|
||||
bugdown_stats_start()
|
||||
ret = do_convert(md, realm)
|
||||
ret = do_convert(md, realm_domain, message)
|
||||
bugdown_stats_finish()
|
||||
return ret
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import re
|
||||
|
||||
from django.db.models import F, Q
|
||||
import zephyr.models
|
||||
|
||||
# Match multi-word string between @** ** or match any one-word
|
||||
# sequences after @
|
||||
find_mentions = r'(?:\B@(?:\*\*([^\*]+)\*\*)|@(\w+))'
|
||||
find_mentions_re = re.compile(find_mentions)
|
||||
|
||||
wildcards = ['all', 'everyone']
|
||||
|
||||
def find_user_for_mention(mention, realm):
|
||||
if mention in wildcards:
|
||||
return (True, None)
|
||||
|
||||
try:
|
||||
user = zephyr.models.UserProfile.objects.filter(
|
||||
Q(full_name__iexact=mention) | Q(short_name__iexact=mention),
|
||||
realm=realm
|
||||
)[0]
|
||||
except IndexError:
|
||||
user = None
|
||||
|
||||
return (False, user)
|
|
@ -339,14 +339,48 @@ class Message(models.Model):
|
|||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def to_dict(self, apply_markdown, rendered_content=None):
|
||||
return extract_message_dict(self.to_dict_json(apply_markdown, rendered_content))
|
||||
def render_markdown(self, content):
|
||||
"""Return HTML for given markdown. Bugdown may add properties to the
|
||||
message object such as `mentions_user_ids` and `mentions_wildcard`.
|
||||
These are only on this Django object and are not saved in the
|
||||
database.
|
||||
"""
|
||||
|
||||
self.mentions_wildcard = False
|
||||
self.mentions_user_ids = set()
|
||||
|
||||
return bugdown.convert(content, self.sender.realm.domain, self)
|
||||
|
||||
def set_rendered_content(self, rendered_content):
|
||||
"""Set the content on the message.
|
||||
|
||||
This method does not call .save(). Save afterwards with
|
||||
update_fields=["rendered_content", "rendered_content_version"].
|
||||
"""
|
||||
|
||||
self.rendered_content = rendered_content
|
||||
self.rendered_content_version = bugdown.version
|
||||
|
||||
if self.rendered_content is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def maybe_render_content(self):
|
||||
"""Render the markdown if there is no existing rendered_content"""
|
||||
if self.rendered_content is None:
|
||||
return self.set_rendered_content(self.render_markdown(self.content))
|
||||
else:
|
||||
return True
|
||||
|
||||
def to_dict(self, apply_markdown):
|
||||
return extract_message_dict(self.to_dict_json(apply_markdown))
|
||||
|
||||
@cache_with_key(to_dict_cache_key, timeout=3600*24)
|
||||
def to_dict_json(self, apply_markdown, rendered_content=None):
|
||||
return stringify_message_dict(self.to_dict_uncached(apply_markdown, rendered_content))
|
||||
def to_dict_json(self, apply_markdown):
|
||||
return stringify_message_dict(self.to_dict_uncached(apply_markdown))
|
||||
|
||||
def to_dict_uncached(self, apply_markdown, rendered_content=None):
|
||||
def to_dict_uncached(self, apply_markdown):
|
||||
display_recipient = get_display_recipient(self.recipient)
|
||||
if self.recipient.type == Recipient.STREAM:
|
||||
display_type = "stream"
|
||||
|
@ -388,16 +422,11 @@ class Message(models.Model):
|
|||
obj['content'] = self.rendered_content
|
||||
obj['content_type'] = 'text/html'
|
||||
elif apply_markdown:
|
||||
if rendered_content is None:
|
||||
rendered_content = bugdown.convert(self.content, self.sender.realm.domain)
|
||||
if rendered_content is None:
|
||||
rendered_content = '<p>[Humbug note: Sorry, we could not understand the formatting of your message]</p>'
|
||||
if self.rendered_content is not None:
|
||||
obj['content'] = self.rendered_content
|
||||
else:
|
||||
obj['content'] = '<p>[Humbug note: Sorry, we could not understand the formatting of your message]</p>'
|
||||
|
||||
# Update the database cache of the rendered content
|
||||
self.rendered_content = rendered_content
|
||||
self.rendered_content_version = bugdown.version
|
||||
self.save(update_fields=["rendered_content", "rendered_content_version"])
|
||||
obj['content'] = rendered_content
|
||||
obj['content_type'] = 'text/html'
|
||||
else:
|
||||
obj['content'] = self.content
|
||||
|
|
|
@ -385,6 +385,15 @@ MessageList.prototype = {
|
|||
if (ids_where_next_is_same_sender[id]) {
|
||||
row.find('.messagebox').addClass("next_is_same_sender");
|
||||
}
|
||||
|
||||
if (row.hasClass('mention')) {
|
||||
row.find('.user-mention').each(function () {
|
||||
var email = $(this).attr('data-user-email');
|
||||
if (email === '*' || email === page_params.email) {
|
||||
$(this).addClass('user-mention-me');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The message that was last before this batch came in has to be
|
||||
|
|
|
@ -1924,6 +1924,20 @@ li.expanded_subject {
|
|||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.user-mention {
|
||||
background: #eee;
|
||||
border-radius: 6px;
|
||||
padding: 0px 0.3em;
|
||||
border: 1px solid #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-mention-me {
|
||||
background: #58F;
|
||||
color: white;
|
||||
border-color: #7080A5;
|
||||
}
|
||||
|
||||
#notification-settings .control-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue