"""Send DM notifications of replies, quote posts, mentions from unbridged users.""" from datetime import timedelta import logging from flask import request from granary import as1 from oauth_dropins.webutil import appengine_info, util from oauth_dropins.webutil.flask_util import cloud_tasks_only import common import dms from memcache import memcache, key from models import PROTOCOLS logger = logging.getLogger(__name__) NOTIFY_TASK_FREQ = timedelta(hours=1) def notification_key(user): return key(f'notifs-{user.key.id()}') def add_notification(user, obj): """Adds a notification for a given user. The memcache key is ``notifs-{user id}``. The value is a space-separated list of object URLs to notify the user of. Uses gets/cas to create the cache entry if it doesn't exist. Args: user (models.User): the user to notify obj (models.Object): the object to notify about """ key = notification_key(user) obj_url = as1.get_url(obj.as1) or obj.key.id() assert obj_url if user.send_notifs != 'all': return # TODO: remove to launch if (user.key.id() not in common.BETA_USER_IDS and not (appengine_info.DEBUG or appengine_info.LOCAL_SERVER)): return if not util.is_web(obj_url): logger.info(f'Dropping non-URL notif {obj_url} for {user.key.id()}') return logger.info(f'Adding notif {obj_url} for {user.key.id()}') if memcache.add(key, obj_url.encode()): common.create_task(queue='notify', delay=NOTIFY_TASK_FREQ, user_id=user.key.id(), protocol=user.LABEL) else: existing = memcache.get(key) if existing and obj_url not in existing.decode().split(): # there's a race condition here if the notify task runs between the gets # call above and this append call, since there won't be a value in # memcache, so append will do nothing. should be rare. # # gets/cas wouldn't make it any easier; we'd still need to keep retrying # until we have a get/append or gets/cas that no one else writes between. memcache.append(key, (' ' + obj_url).encode()) def get_notifications(user, clear=False): """Gets enqueued notifications for a given user. The memcache key is ``notifs-{user id}``. Args: user (models.User) clear (bool): clear notifications from memcache after fetching them Returns: list of str: URLs to notify the user of; possibly empty """ key = notification_key(user) notifs = memcache.get(key, default=b'').decode().strip().split() if notifs and clear: memcache.delete(key) return notifs @cloud_tasks_only() def notify_task(): """Task handler for sending a notification DM to a user. Fetches notifications from memcache. Parameters: user_id (str): ID of the user to send notifications to protocol (str): Protocol label the user is on """ common.log_request() proto = PROTOCOLS[request.form['protocol']] user_id = request.form['user_id'] if not (user := proto.get_by_id(user_id)): logger.info(f"Couldn't load user {user_id}") return '', 204 if not (notifs := get_notifications(user, clear=True)): logger.info(f'No notifications for {user_id}') return '', 204 from_proto_label = (user.enabled_protocols[0] if user.enabled_protocols else user.DEFAULT_ENABLED_PROTOCOLS[0] if user.DEFAULT_ENABLED_PROTOCOLS else None) if not from_proto_label: logger.info(f"User {user_id} isn't enabled") return '', 204 message = f"
Hi! Here are your recent interactions from people who aren't bridged into {user.PHRASE}:\n
To disable these messages, reply with the text 'mute'." logger.info(f'sending notifications DM for {user_id}') dms.maybe_send(from_proto=PROTOCOLS[from_proto_label], to_user=user, text=message) return '', 200