Add user and package mentions

This commit is contained in:
rubenwardy 2022-01-17 15:06:03 +00:00
parent e02c014890
commit 6a4bf7129d
2 changed files with 82 additions and 13 deletions

View File

@ -16,6 +16,7 @@
from flask import * from flask import *
from flask_babel import gettext, lazy_gettext from flask_babel import gettext, lazy_gettext
from app.markdown import get_user_mentions, render_markdown
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
bp = Blueprint("threads", __name__) bp = Blueprint("threads", __name__)
@ -238,6 +239,15 @@ def view(id):
if not current_user in thread.watchers: if not current_user in thread.watchers:
thread.watchers.append(current_user) thread.watchers.append(current_user)
for mentioned_username in get_user_mentions(render_markdown(comment)):
mentioned = User.query.filter_by(username=mentioned_username)
if mentioned is None:
continue
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
addNotification(mentioned, current_user, NotificationType.THREAD_REPLY,
msg, thread.getViewURL(), thread.package)
msg = "New comment on '{}'".format(thread.title) msg = "New comment on '{}'".format(thread.title)
addNotification(thread.watchers, current_user, NotificationType.THREAD_REPLY, msg, thread.getViewURL(), thread.package) addNotification(thread.watchers, current_user, NotificationType.THREAD_REPLY, msg, thread.getViewURL(), thread.package)
@ -335,6 +345,15 @@ def new():
if is_review_thread: if is_review_thread:
package.review_thread = thread package.review_thread = thread
for mentioned_username in get_user_mentions(render_markdown(form.comment.data)):
mentioned = User.query.filter_by(username=mentioned_username)
if mentioned is None:
continue
msg = "Mentioned by {} in new thread '{}'".format(current_user.display_name, thread.title)
addNotification(mentioned, current_user, NotificationType.NEW_THREAD,
msg, thread.getViewURL(), thread.package)
notif_msg = "New thread '{}'".format(thread.title) notif_msg = "New thread '{}'".format(thread.title)
if package is not None: if package is not None:
addNotification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.getViewURL(), package) addNotification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.getViewURL(), package)
@ -342,6 +361,7 @@ def new():
approvers = User.query.filter(User.rank >= UserRank.APPROVER).all() approvers = User.query.filter(User.rank >= UserRank.APPROVER).all()
addNotification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.getViewURL(), package) addNotification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.getViewURL(), package)
if is_review_thread: if is_review_thread:
post_discord_webhook.delay(current_user.username, post_discord_webhook.delay(current_user.username,
"Opened approval thread: {}".format(thread.getViewURL(absolute=True)), True) "Opened approval thread: {}".format(thread.getViewURL(absolute=True)), True)

View File

@ -5,10 +5,11 @@ from bleach import Cleaner
from bleach.linkifier import LinkifyFilter from bleach.linkifier import LinkifyFilter
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from markdown import Markdown from markdown import Markdown
from flask import Markup from flask import Markup, url_for
from markdown.extensions import Extension from markdown.extensions import Extension
from markdown.inlinepatterns import SimpleTagInlineProcessor from markdown.inlinepatterns import SimpleTagInlineProcessor
from markdown.inlinepatterns import Pattern
from xml.etree import ElementTree
# Based on # Based on
# https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py # https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py
@ -40,15 +41,17 @@ ALLOWED_CSS = [
"s2", "se", "sh", "si", "sx", "sr", "s1", "ss", "bp", "fm", "vc", "vg", "vi", "vm", "il", "s2", "se", "sh", "si", "sx", "sr", "s1", "ss", "bp", "fm", "vc", "vg", "vi", "vm", "il",
] ]
def allow_class(_tag, name, value): def allow_class(_tag, name, value):
return name == "class" and value in ALLOWED_CSS return name == "class" and value in ALLOWED_CSS
ALLOWED_ATTRIBUTES = { ALLOWED_ATTRIBUTES = {
"h1": ["id"], "h1": ["id"],
"h2": ["id"], "h2": ["id"],
"h3": ["id"], "h3": ["id"],
"h4": ["id"], "h4": ["id"],
"a": ["href", "title"], "a": ["href", "title", "data-username"],
"img": ["src", "title", "alt"], "img": ["src", "title", "alt"],
"code": allow_class, "code": allow_class,
"div": allow_class, "div": allow_class,
@ -64,23 +67,63 @@ def render_markdown(source):
html = md.convert(source) html = md.convert(source)
cleaner = Cleaner( cleaner = Cleaner(
tags=ALLOWED_TAGS, tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES, attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, protocols=ALLOWED_PROTOCOLS,
filters=[partial(LinkifyFilter, callbacks=bleach.linkifier.DEFAULT_CALLBACKS)]) filters=[partial(LinkifyFilter, callbacks=bleach.linkifier.DEFAULT_CALLBACKS)])
return cleaner.clean(html) return cleaner.clean(html)
class DelInsExtension(Extension): class DelInsExtension(Extension):
def extendMarkdown(self, md): def extendMarkdown(self, md):
del_proc = SimpleTagInlineProcessor(r'(\~\~)(.+?)(\~\~)', 'del') del_proc = SimpleTagInlineProcessor(r"(\~\~)(.+?)(\~\~)", "del")
md.inlinePatterns.register(del_proc, 'del', 200) md.inlinePatterns.register(del_proc, "del", 200)
ins_proc = SimpleTagInlineProcessor(r'(\+\+)(.+?)(\+\+)', 'ins') ins_proc = SimpleTagInlineProcessor(r"(\+\+)(.+?)(\+\+)", "ins")
md.inlinePatterns.register(ins_proc, 'ins', 200) md.inlinePatterns.register(ins_proc, "ins", 200)
MARKDOWN_EXTENSIONS = ["fenced_code", "tables", "codehilite", "toc", DelInsExtension()] RE_PARTS = dict(
USER=r"[A-Za-z0-9._-]*\b",
REPO=r"[A-Za-z0-9_]+\b"
)
class MentionPattern(Pattern):
ANCESTOR_EXCLUDES = ("a",)
def __init__(self, config, md):
MENTION_RE = r"(@({USER})(?:\/({REPO}))?)".format(**RE_PARTS)
super(MentionPattern, self).__init__(MENTION_RE, md)
self.config = config
def handleMatch(self, m):
label = m.group(2)
user = m.group(3)
package_name = m.group(4)
if package_name:
el = ElementTree.Element("a")
el.text = label
el.set("href", url_for("packages.view", author=user, name=package_name))
return el
else:
el = ElementTree.Element("a")
el.text = label
el.set("href", url_for("users.profile", username=user))
el.set("data-username", user)
return el
class MentionExtension(Extension):
def __init__(self, *args, **kwargs):
super(MentionExtension, self).__init__(*args, **kwargs)
def extendMarkdown(self, md):
md.ESCAPED_CHARS.append("@")
md.inlinePatterns.register(MentionPattern(self.getConfigs(), md), "mention", 20)
MARKDOWN_EXTENSIONS = ["fenced_code", "tables", "codehilite", "toc", DelInsExtension(), MentionExtension()]
MARKDOWN_EXTENSION_CONFIG = { MARKDOWN_EXTENSION_CONFIG = {
"fenced_code": {}, "fenced_code": {},
"tables": {}, "tables": {},
@ -109,7 +152,7 @@ def get_headings(html: str):
root = [] root = []
stack = [] stack = []
for heading in headings: for heading in headings:
this = { "link": heading.get("id") or "", "text": heading.text, "children": [] } this = {"link": heading.get("id") or "", "text": heading.text, "children": []}
this_level = int(heading.name[1:]) - 1 this_level = int(heading.name[1:]) - 1
while this_level <= len(stack): while this_level <= len(stack):
@ -123,3 +166,9 @@ def get_headings(html: str):
stack.append(this) stack.append(this)
return root return root
def get_user_mentions(html: str) -> set:
soup = BeautifulSoup(html, "html.parser")
links = soup.select("a[data-username]")
return set([x.get("data-username") for x in links])