From 0dae7b688cdbdccc6ec9a8ad1786fb672f38ea35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaakko=20Kera=CC=88nen?= Date: Mon, 15 May 2023 21:51:02 +0300 Subject: [PATCH 1/1] Fixes and improvements for notifications @-mentions will override and clear out earlier commenting notifications. Improved the display of issue notifications. Posts without titles will now be notified about using a shortened summary. IssueID #40 --- model.py | 90 ++++++++++++++++++++++++++++++++++--------------------- worker.py | 36 ++++++++++++++-------- 2 files changed, 79 insertions(+), 47 deletions(-) diff --git a/model.py b/model.py index 36fcc16..b3ec6c3 100644 --- a/model.py +++ b/model.py @@ -51,7 +51,7 @@ class Notification: ISSUE_CLOSED = 0x200 def __init__(self, id, type, dst, src, post, is_sent, ts, - src_name=None, post_title=None, post_issueid=None, + src_name=None, post_title=None, post_issueid=None, post_summary=None, post_subname=None, post_subowner=None): self.id = id self.type = type @@ -63,6 +63,7 @@ class Notification: self.src_name = src_name self.post_title = post_title self.post_issueid = post_issueid + self.post_summary = post_summary self.post_subname = post_subname self.post_subowner = post_subowner @@ -78,24 +79,25 @@ class Notification: event = '' icon = '๐Ÿ”” ' + kind = 'issue' if self.post_issueid else 'post' if self.type == Notification.LIKE: - event = 'liked your post' + event = f'liked your {kind}' icon = '๐Ÿ‘ ' elif self.type == Notification.COMMENT: - event = 'commented on your post' + event = f'commented on your {kind}' icon = '๐Ÿ’ฌ ' elif self.type == Notification.COMMENT_ON_COMMENTED: event = 'commented in a thread you are part of' icon = '๐Ÿ’ฌ ' elif self.type == Notification.COMMENT_ON_FOLLOWED_POST: - event = 'commented on followed post' + event = f'commented on followed {kind}' icon = '๐Ÿ’ฌ ' elif self.type == Notification.COMMENT_BY_FOLLOWED_USER: - event = 'commented on post' + event = f'commented on {kind}' icon = '๐Ÿ’ฌ ' elif self.type == Notification.POST_BY_FOLLOWED_USER: - event = 'published new a post' + event = f'published new {kind}' elif self.type == Notification.POST_IN_FOLLOWED_SUBSPACE: event = f'posted in followed subspace {"u/" if self.post_subowner else "s/"}{self.post_subname}' elif self.type == Notification.MENTION: @@ -104,15 +106,21 @@ class Notification: event = 'posted a new poll' icon = '๐Ÿ—ณ๏ธ ' elif self.type == Notification.ISSUE_CLOSED: - event = f'closed issue #{self.post_issueid}' + event = f'closed issue' icon = 'โœ”๏ธŽ ' - if self.post_title: + vis_title = self.post_title if self.post_title else \ + shorten_text(self.post_summary, 50) if self.post_summary else None + + if vis_title: if self.type == Notification.MENTION: event += ' in' - elif self.type == Notification.COMMENT_ON_COMMENTED: + elif self.type in (Notification.COMMENT_ON_COMMENTED, + Notification.POST_IN_FOLLOWED_SUBSPACE): event += ', in' - event += f' "{self.post_title}"' + if self.post_issueid: + event += f" #{self.post_issueid}" + event += f' "{vis_title}"' age = f' ยท {self.age()}' if show_age else '' return f'/notif/{self.id}', f'{icon}{self.src_name} {event}{age}' @@ -897,7 +905,8 @@ class Database: if parent: cur.execute(Database.NUM_CMTS_QUERY, (parent, parent)) - # Notify the author of the parent post. + # Notify the author of the parent post. We can do it here because comments are + # never drafted. They become published immediately after creation. parent_post = self.get_post(id=parent) if parent_post.user != user.id: cur.execute("INSERT IGNORE INTO notifs (type, dst, src, post) VALUES (?, ?, ?, ?)", @@ -909,13 +918,6 @@ class Database: self.notify_followers(user, parent_post.id, Notification.COMMENT_ON_FOLLOWED_POST, FOLLOW_POST, parent_post.id) - else: - self.notify_followers(user, post_id, - Notification.POST_BY_FOLLOWED_USER, - FOLLOW_USER, user.id) - self.notify_followers(user, post_id, - Notification.POST_IN_FOLLOWED_SUBSPACE, - FOLLOW_SUBSPACE, subspace_id) return post_id @@ -1004,6 +1006,16 @@ class Database: if Post.TAG_POLL in self.get_tags(post): self.notify_new_poll(post) + # Notify about the new published post (but not a comment). + if not post.parent: + user = self.get_user(id=post.user) + self.notify_followers(user, post.id, + Notification.POST_BY_FOLLOWED_USER, + FOLLOW_USER, user.id) + self.notify_followers(user, post.id, + Notification.POST_IN_FOLLOWED_SUBSPACE, + FOLLOW_SUBSPACE, post.subspace) + if self.get_subspace(post.subspace).flags & Subspace.ISSUE_TRACKER and \ post.issueid is None and \ post.parent == 0: @@ -1405,19 +1417,28 @@ class Database: names = parse_at_names(content) if names: cur = self.conn.cursor() - cur.execute("SELECT id FROM users " + - f"WHERE id!={post.user} AND name IN ({('?,' * len(names))[:-1]})", - tuple(names)) - uids = [] - for (i,) in cur: - uids.append(i) - for uid in uids: - cur.execute(""" - INSERT IGNORE INTO notifs (type, dst, src, post) - VALUES (?, ?, ?, ?) - """, (Notification.MENTION, uid, post.user, post.parent if post.parent else post.id)) - if uids: - self.commit() + notif_post_id = post.parent if post.parent else post.id + where_names_cond = f"name IN ({('?,' * len(names))[:-1]})" + cur.execute(f""" + INSERT IGNORE INTO notifs (type, dst, src, post) + SELECT + {Notification.MENTION}, id, {post.user}, {notif_post_id} + FROM users + WHERE id!={post.user} AND {where_names_cond} + """, names) + # Remove redundant, less important notifications. + cur.execute(f""" + DELETE FROM notifs + WHERE (type={Notification.COMMENT_ON_COMMENTED} OR + type={Notification.COMMENT} OR + type={Notification.COMMENT_BY_FOLLOWED_USER}) AND + post={notif_post_id} AND + src={post.user} AND + dst IN ( + SELECT id FROM users WHERE {where_names_cond} + ) + """, names) + self.commit() def notify_commenters(self, new_comment): """When `new_comment` is posted, notify other participants of the same thread about it.""" @@ -1574,7 +1595,7 @@ class Database: cur.execute(f""" SELECT n.id, n.type, n.dst, n.src, n.post, n.is_sent, UNIX_TIMESTAMP(n.ts), - u.name, p.title, p.issueid, s.name, s.owner + u.name, p.title, p.issueid, p.summary, s.name, s.owner FROM notifs n JOIN users u ON src=u.id JOIN posts p ON post=p.id @@ -1583,9 +1604,10 @@ class Database: ORDER BY ts """, tuple(values)) notifs = [] - for (id, type, dst, src, post, is_sent, ts, src_name, post_title, post_issueid, post_subname, post_subowner) in cur: + for (id, type, dst, src, post, is_sent, ts, src_name, post_title, post_issueid, + post_summary, post_subname, post_subowner) in cur: notifs.append(Notification(id, type, dst, src, post, is_sent, ts, - src_name, post_title, post_issueid, + src_name, post_title, post_issueid, post_summary, post_subname, post_subowner)) if clear and notifs: cur.execute(f"DELETE FROM notifs WHERE id IN ({','.join(map(lambda n: str(n.id), notifs))})", list()) diff --git a/worker.py b/worker.py index 444bcb0..10474e4 100644 --- a/worker.py +++ b/worker.py @@ -55,25 +55,29 @@ class Emailer (threading.Thread): for id, name, email, enabled_types in pending_notifs: cur.execute(""" SELECT - notifs.id, type, dst, src, post, - UNIX_TIMESTAMP(ts), su.name, p.title - FROM notifs - JOIN users su ON src=su.id - JOIN posts p ON post=p.id - WHERE dst=? AND is_sent=FALSE AND type & ? - ORDER BY ts + n.id, n.type, n.dst, n.src, n.post, + UNIX_TIMESTAMP(n.ts), su.name, p.title, p.issueid, p.summary, + s.name, s.owner + FROM notifs n + JOIN users su ON n.src=su.id + JOIN posts p ON n.post=p.id + JOIN subspaces s ON p.subspace=s.id + WHERE n.dst=? AND n.is_sent=FALSE AND (n.type & ?) + ORDER BY n.ts """, (id, enabled_types)) count = 0 body = f'Hello {name}!\n\n' - body += f'There has been activity on {self.site_name}:\n\n' + body += f'There has been activity on {self.site_name}:\n' - for (notif_id, type, dst, src, post, ts, src_name, post_title) in cur: + for (notif_id, type, dst, src, post, ts, src_name, post_title, post_issueid, + post_summary, post_subname, post_subowner) in cur: notif = Notification(notif_id, type, dst, src, post, False, ts, - src_name, post_title) + src_name, post_title, post_issueid, post_summary, + post_subname, post_subowner) count += 1 _, label = notif.entry(show_age=False) - body += '* ' + label + '\n' + body += '\n ' + label + '\n' if count: subject = f'{name}: {count} new notification{plural_s(count)}' @@ -99,8 +103,10 @@ class Emailer (threading.Thread): body args = [self.email_cmd, '-i', email] - #print(args, msg) - subprocess.check_output(args, input=msg, encoding='utf-8') + if self.email_cmd == 'stdout': + print(args, msg) + else: + subprocess.check_output(args, input=msg, encoding='utf-8') except Exception as x: print('Emailer error:', x) @@ -108,10 +114,14 @@ class Emailer (threading.Thread): if not self.email_cmd: # Emailter is disabled. return + print("Emailer is running") while not self.capsule.shutdown_event().wait(self.email_interval): db = Database(self.cfg) try: self.send_notifications(db) + except: + import traceback + traceback.print_last() finally: db.close() -- 2.34.1