mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-12-22 13:14:28 +00:00
Improve notifications
- Keep track of unread count - "follow back" action
This commit is contained in:
parent
2ab59d9476
commit
d70c73cad7
9 changed files with 261 additions and 36 deletions
|
@ -30,6 +30,7 @@ from config import ME
|
||||||
from config import USER_AGENT
|
from config import USER_AGENT
|
||||||
from config import USERNAME
|
from config import USERNAME
|
||||||
from tasks import Tasks
|
from tasks import Tasks
|
||||||
|
from utils.meta import Box
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -95,12 +96,6 @@ def _is_local_reply(create: ap.Create) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Box(Enum):
|
|
||||||
INBOX = "inbox"
|
|
||||||
OUTBOX = "outbox"
|
|
||||||
REPLIES = "replies"
|
|
||||||
|
|
||||||
|
|
||||||
class MicroblogPubBackend(Backend):
|
class MicroblogPubBackend(Backend):
|
||||||
"""Implements a Little Boxes backend, backed by MongoDB."""
|
"""Implements a Little Boxes backend, backed by MongoDB."""
|
||||||
|
|
||||||
|
|
93
app.py
93
app.py
|
@ -84,14 +84,18 @@ from config import USER_AGENT
|
||||||
from config import USERNAME
|
from config import USERNAME
|
||||||
from config import VERSION
|
from config import VERSION
|
||||||
from config import VERSION_DATE
|
from config import VERSION_DATE
|
||||||
|
from config import MetaKey
|
||||||
from config import _drop_db
|
from config import _drop_db
|
||||||
|
from config import _meta
|
||||||
from poussetaches import PousseTaches
|
from poussetaches import PousseTaches
|
||||||
from tasks import Tasks
|
from tasks import Tasks
|
||||||
|
from utils import now
|
||||||
from utils import opengraph
|
from utils import opengraph
|
||||||
from utils import parse_datetime
|
from utils import parse_datetime
|
||||||
from utils.key import get_secret_key
|
from utils.key import get_secret_key
|
||||||
from utils.lookup import lookup
|
from utils.lookup import lookup
|
||||||
from utils.media import Kind
|
from utils.media import Kind
|
||||||
|
from utils.notifications import set_inbox_flags
|
||||||
|
|
||||||
p = PousseTaches(
|
p = PousseTaches(
|
||||||
os.getenv("MICROBLOGPUB_POUSSETACHES_HOST", "http://localhost:7991"),
|
os.getenv("MICROBLOGPUB_POUSSETACHES_HOST", "http://localhost:7991"),
|
||||||
|
@ -179,6 +183,7 @@ def inject_config():
|
||||||
"type": ActivityType.FOLLOW.value,
|
"type": ActivityType.FOLLOW.value,
|
||||||
"meta.undo": False,
|
"meta.undo": False,
|
||||||
}
|
}
|
||||||
|
unread_notifications_q = {_meta(MetaKey.NOTIFICATION_UNREAD): True}
|
||||||
|
|
||||||
logged_in = session.get("logged_in", False)
|
logged_in = session.get("logged_in", False)
|
||||||
|
|
||||||
|
@ -191,6 +196,9 @@ def inject_config():
|
||||||
notes_count=notes_count,
|
notes_count=notes_count,
|
||||||
liked_count=liked_count,
|
liked_count=liked_count,
|
||||||
with_replies_count=DB.activities.count(all_q) if logged_in else 0,
|
with_replies_count=DB.activities.count(all_q) if logged_in else 0,
|
||||||
|
unread_notifications_count=DB.activities.count(unread_notifications_q)
|
||||||
|
if logged_in
|
||||||
|
else 0,
|
||||||
me=ME,
|
me=ME,
|
||||||
base_url=config.BASE_URL,
|
base_url=config.BASE_URL,
|
||||||
)
|
)
|
||||||
|
@ -695,6 +703,21 @@ def serve_uploads(oid, fname):
|
||||||
# Login
|
# Login
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/update_actor")
|
||||||
|
@login_required
|
||||||
|
def admin_update_actor():
|
||||||
|
update = ap.Update(
|
||||||
|
actor=MY_PERSON.id,
|
||||||
|
object=MY_PERSON.to_dict(),
|
||||||
|
to=[MY_PERSON.followers],
|
||||||
|
cc=[ap.AS_PUBLIC],
|
||||||
|
published=now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
post_to_outbox(update)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/logout")
|
@app.route("/admin/logout")
|
||||||
@login_required
|
@login_required
|
||||||
def admin_logout():
|
def admin_logout():
|
||||||
|
@ -783,11 +806,7 @@ def authorize_follow():
|
||||||
return redirect("/following")
|
return redirect("/following")
|
||||||
|
|
||||||
follow = ap.Follow(
|
follow = ap.Follow(
|
||||||
actor=MY_PERSON.id,
|
actor=MY_PERSON.id, object=actor, to=[actor], cc=[ap.AS_PUBLIC], published=now()
|
||||||
object=actor,
|
|
||||||
to=[actor],
|
|
||||||
cc=[ap.AS_PUBLIC],
|
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
|
||||||
)
|
)
|
||||||
post_to_outbox(follow)
|
post_to_outbox(follow)
|
||||||
|
|
||||||
|
@ -1649,17 +1668,23 @@ def admin_notifications():
|
||||||
.sort("_id", -1)
|
.sort("_id", -1)
|
||||||
.limit(50)
|
.limit(50)
|
||||||
)
|
)
|
||||||
|
print(inbox_data)
|
||||||
|
|
||||||
|
nid = None
|
||||||
|
if inbox_data:
|
||||||
|
nid = inbox_data[0]["_id"]
|
||||||
|
|
||||||
inbox_data.extend(notifs)
|
inbox_data.extend(notifs)
|
||||||
inbox_data = sorted(
|
inbox_data = sorted(
|
||||||
inbox_data, reverse=True, key=lambda doc: doc["_id"].generation_time
|
inbox_data, reverse=True, key=lambda doc: doc["_id"].generation_time
|
||||||
)
|
)
|
||||||
print(inbox_data)
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"stream.html",
|
"stream.html",
|
||||||
inbox_data=inbox_data,
|
inbox_data=inbox_data,
|
||||||
older_than=older_than,
|
older_than=older_than,
|
||||||
newer_than=newer_than,
|
newer_than=newer_than,
|
||||||
|
nid=nid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1721,7 +1746,7 @@ def api_delete():
|
||||||
object=ap.Tombstone(id=note.id).to_dict(embed=True),
|
object=ap.Tombstone(id=note.id).to_dict(embed=True),
|
||||||
to=note.to,
|
to=note.to,
|
||||||
cc=note.cc,
|
cc=note.cc,
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
published=now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
delete_id = post_to_outbox(delete)
|
delete_id = post_to_outbox(delete)
|
||||||
|
@ -1743,13 +1768,26 @@ def api_boost():
|
||||||
object=note.id,
|
object=note.id,
|
||||||
to=[MY_PERSON.followers, note.attributedTo],
|
to=[MY_PERSON.followers, note.attributedTo],
|
||||||
cc=[ap.AS_PUBLIC],
|
cc=[ap.AS_PUBLIC],
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
published=now(),
|
||||||
)
|
)
|
||||||
announce_id = post_to_outbox(announce)
|
announce_id = post_to_outbox(announce)
|
||||||
|
|
||||||
return _user_api_response(activity=announce_id)
|
return _user_api_response(activity=announce_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/mark_notifications_as_read", methods=["POST"])
|
||||||
|
@api_required
|
||||||
|
def api_mark_notification_as_read():
|
||||||
|
nid = ObjectId(_user_api_arg("nid"))
|
||||||
|
|
||||||
|
DB.activities.update_many(
|
||||||
|
{_meta(MetaKey.NOTIFICATION_UNREAD): True, "_id": {"$lte": nid}},
|
||||||
|
{"$set": {_meta(MetaKey.NOTIFICATION_UNREAD): False}},
|
||||||
|
)
|
||||||
|
|
||||||
|
return _user_api_response()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/vote", methods=["POST"])
|
@app.route("/api/vote", methods=["POST"])
|
||||||
@api_required
|
@api_required
|
||||||
def api_vote():
|
def api_vote():
|
||||||
|
@ -1794,13 +1832,7 @@ def api_like():
|
||||||
else:
|
else:
|
||||||
to = [note.get_actor().id]
|
to = [note.get_actor().id]
|
||||||
|
|
||||||
like = ap.Like(
|
like = ap.Like(object=note.id, actor=MY_PERSON.id, to=to, cc=cc, published=now())
|
||||||
object=note.id,
|
|
||||||
actor=MY_PERSON.id,
|
|
||||||
to=to,
|
|
||||||
cc=cc,
|
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
|
||||||
)
|
|
||||||
|
|
||||||
like_id = post_to_outbox(like)
|
like_id = post_to_outbox(like)
|
||||||
|
|
||||||
|
@ -1870,7 +1902,7 @@ def api_undo():
|
||||||
undo = ap.Undo(
|
undo = ap.Undo(
|
||||||
actor=MY_PERSON.id,
|
actor=MY_PERSON.id,
|
||||||
object=obj.to_dict(embed=True, embed_object_id_only=True),
|
object=obj.to_dict(embed=True, embed_object_id_only=True),
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
published=now(),
|
||||||
to=obj.to,
|
to=obj.to,
|
||||||
cc=obj.cc,
|
cc=obj.cc,
|
||||||
)
|
)
|
||||||
|
@ -2360,11 +2392,7 @@ def api_follow():
|
||||||
return _user_api_response(activity=existing["activity"]["id"])
|
return _user_api_response(activity=existing["activity"]["id"])
|
||||||
|
|
||||||
follow = ap.Follow(
|
follow = ap.Follow(
|
||||||
actor=MY_PERSON.id,
|
actor=MY_PERSON.id, object=actor, to=[actor], cc=[ap.AS_PUBLIC], published=now()
|
||||||
object=actor,
|
|
||||||
to=[actor],
|
|
||||||
cc=[ap.AS_PUBLIC],
|
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
|
||||||
)
|
)
|
||||||
follow_id = post_to_outbox(follow)
|
follow_id = post_to_outbox(follow)
|
||||||
|
|
||||||
|
@ -2904,12 +2932,17 @@ def task_finish_post_to_inbox():
|
||||||
back.inbox_like(MY_PERSON, activity)
|
back.inbox_like(MY_PERSON, activity)
|
||||||
elif activity.has_type(ap.ActivityType.FOLLOW):
|
elif activity.has_type(ap.ActivityType.FOLLOW):
|
||||||
# Reply to a Follow with an Accept
|
# Reply to a Follow with an Accept
|
||||||
|
actor_id = activity.get_actor().id
|
||||||
accept = ap.Accept(
|
accept = ap.Accept(
|
||||||
actor=ID,
|
actor=ID,
|
||||||
object=activity.to_dict(),
|
object={
|
||||||
to=[activity.get_actor().id],
|
"type": "Follow",
|
||||||
cc=[ap.AS_PUBLIC],
|
"id": activity.id,
|
||||||
published=ap.format_datetime(datetime.now(timezone.utc)),
|
"object": activity.get_object_id(),
|
||||||
|
"actor": actor_id,
|
||||||
|
},
|
||||||
|
to=[actor_id],
|
||||||
|
published=now(),
|
||||||
)
|
)
|
||||||
post_to_outbox(accept)
|
post_to_outbox(accept)
|
||||||
elif activity.has_type(ap.ActivityType.UNDO):
|
elif activity.has_type(ap.ActivityType.UNDO):
|
||||||
|
@ -3095,6 +3128,16 @@ def task_process_new_activity():
|
||||||
should_delete = False
|
should_delete = False
|
||||||
should_keep = False
|
should_keep = False
|
||||||
|
|
||||||
|
flags = {}
|
||||||
|
|
||||||
|
if not activity.published:
|
||||||
|
flags[_meta(MetaKey.PUBLISHED)] = now()
|
||||||
|
|
||||||
|
set_inbox_flags(activity, flags)
|
||||||
|
app.logger.info(f"a={activity}, flags={flags!r}")
|
||||||
|
if flags:
|
||||||
|
DB.activities.update_one({"remote_id": activity.id}, {"$set": flags})
|
||||||
|
|
||||||
tag_stream = False
|
tag_stream = False
|
||||||
if activity.has_type(ap.ActivityType.ANNOUNCE):
|
if activity.has_type(ap.ActivityType.ANNOUNCE):
|
||||||
# FIXME(tsileo): Ensure it's follower and store into a "dead activities" DB
|
# FIXME(tsileo): Ensure it's follower and store into a "dead activities" DB
|
||||||
|
|
|
@ -16,6 +16,8 @@ from utils.key import KEY_DIR
|
||||||
from utils.key import get_key
|
from utils.key import get_key
|
||||||
from utils.key import get_secret_key
|
from utils.key import get_secret_key
|
||||||
from utils.media import MediaCache
|
from utils.media import MediaCache
|
||||||
|
from utils.meta import MetaKey
|
||||||
|
from utils.meta import _meta
|
||||||
|
|
||||||
|
|
||||||
class ThemeStyle(Enum):
|
class ThemeStyle(Enum):
|
||||||
|
@ -111,6 +113,10 @@ def create_indexes():
|
||||||
DB.create_collection("trash", capped=True, size=50 << 20) # 50 MB
|
DB.create_collection("trash", capped=True, size=50 << 20) # 50 MB
|
||||||
|
|
||||||
DB.command("compact", "activities")
|
DB.command("compact", "activities")
|
||||||
|
DB.activities.create_index([(_meta(MetaKey.NOTIFICATION), pymongo.ASCENDING)])
|
||||||
|
DB.activities.create_index(
|
||||||
|
[(_meta(MetaKey.NOTIFICATION_UNREAD), pymongo.ASCENDING)]
|
||||||
|
)
|
||||||
DB.activities.create_index([("remote_id", pymongo.ASCENDING)])
|
DB.activities.create_index([("remote_id", pymongo.ASCENDING)])
|
||||||
DB.activities.create_index([("activity.object.id", pymongo.ASCENDING)])
|
DB.activities.create_index([("activity.object.id", pymongo.ASCENDING)])
|
||||||
DB.activities.create_index([("meta.thread_root_parent", pymongo.ASCENDING)])
|
DB.activities.create_index([("meta.thread_root_parent", pymongo.ASCENDING)])
|
||||||
|
|
|
@ -244,13 +244,24 @@ a:hover {
|
||||||
background: $color-menu-background;
|
background: $color-menu-background;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
color: $color-light;
|
color: $color-light;
|
||||||
margin-right:5px;
|
margin-right:10px;
|
||||||
border-radius:2px;
|
border-radius:2px;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
.bar-item-no-hover:hover {
|
.bar-item-no-hover:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
.bar-item-no-bg {
|
||||||
|
cursor: default;
|
||||||
|
padding: 5px;
|
||||||
|
color: $color-light;
|
||||||
|
margin-right:10px;
|
||||||
|
border-radius:2px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.bar-item-no-bg:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
.bar-item-pinned {
|
.bar-item-pinned {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
background: $color-menu-background;
|
background: $color-menu-background;
|
||||||
|
|
|
@ -27,7 +27,10 @@
|
||||||
<li class="left"><a href="/" class="admin-title {% if not request.path.startswith("/admin") %} selected{% endif %}">Public</a></li>
|
<li class="left"><a href="/" class="admin-title {% if not request.path.startswith("/admin") %} selected{% endif %}">Public</a></li>
|
||||||
<li class="left"><a href="/admin/new"{% if request.path == "/admin/new" %} class="selected" {% endif %}>New</a></li>
|
<li class="left"><a href="/admin/new"{% if request.path == "/admin/new" %} class="selected" {% endif %}>New</a></li>
|
||||||
<li class="left"><a href="/admin/stream"{% if request.path == "/admin/stream" %} class="selected" {% endif %}>Stream</a></li>
|
<li class="left"><a href="/admin/stream"{% if request.path == "/admin/stream" %} class="selected" {% endif %}>Stream</a></li>
|
||||||
<li class="left"><a href="/admin/notifications"{% if request.path == "/admin/notifications" %} class="selected" {% endif %}>Notifications</a></li>
|
<li class="left"><a href="/admin/notifications"{% if request.path == "/admin/notifications" %} class="selected" {% endif %}>Notifications
|
||||||
|
{% if unread_notifications_count %}
|
||||||
|
({{unread_notifications_count}})
|
||||||
|
{% endif %}</a></li>
|
||||||
<li class="left"><a href="/admin/lists"{% if request.path == url_for('admin_lists') %} class="selected" {% endif %}>Lists</a></li>
|
<li class="left"><a href="/admin/lists"{% if request.path == url_for('admin_lists') %} class="selected" {% endif %}>Lists</a></li>
|
||||||
<li class="left"><a href="/admin/bookmarks"{% if request.path == "/admin/bookmarks" %} class="selected" {% endif %}>Bookmarks</a></li>
|
<li class="left"><a href="/admin/bookmarks"{% if request.path == "/admin/bookmarks" %} class="selected" {% endif %}>Bookmarks</a></li>
|
||||||
<li class="left"><a href="/admin/lookup"{% if request.path == "/admin/lookup" %} class="selected" {% endif %}>Lookup</a></li>
|
<li class="left"><a href="/admin/lookup"{% if request.path == "/admin/lookup" %} class="selected" {% endif %}>Lookup</a></li>
|
||||||
|
|
|
@ -6,7 +6,18 @@
|
||||||
{% include "header.html" %}
|
{% include "header.html" %}
|
||||||
<div id="admin">
|
<div id="admin">
|
||||||
|
|
||||||
<div id="notes">
|
{% if request.path == url_for('admin_notifications') and unread_notifications_count %}
|
||||||
|
<div style="clear:both;padding-bottom:30px;">
|
||||||
|
<form action="/api/mark_notifications_as_read" method="POST">
|
||||||
|
<input type="hidden" name="redirect" value="{{ request.path }}"/>
|
||||||
|
<input type="hidden" name="nid" value="{{ nid }}"/>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" class="bar-item" style="text-transform:uppercase">Mark as read</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="notes" style="clear:both;">
|
||||||
{% for item in inbox_data %}
|
{% for item in inbox_data %}
|
||||||
{% if 'actor' in item.meta %}
|
{% if 'actor' in item.meta %}
|
||||||
{% if item | has_type('Create') %}
|
{% if item | has_type('Create') %}
|
||||||
|
@ -31,13 +42,30 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if item | has_type('Follow') %}
|
{% if item | has_type('Follow') %}
|
||||||
<p style="margin-left:70px;padding-bottom:5px;display:inline-block;"><span class="bar-item-no-hover">new follower</span> <!-- <a href="" class="bar-item">follow back</a></p> -->
|
<div style="margin-left:70px;padding-bottom:5px;margin-bottom:15px;display:inline-block;">
|
||||||
|
{% if item.meta.notification_unread %}<span class="bar-item-no-bg"><span class="pcolor">new</span></span>{% endif %}
|
||||||
|
<span class="bar-item-no-bg">new follower</span>
|
||||||
|
{% if item.meta.notification_follows_back %}<span class="bar-item-no-hover">already following</span>
|
||||||
|
{% else %}
|
||||||
|
<form action="/api/follow" class="action-form" method="POST">
|
||||||
|
<input type="hidden" name="redirect" value="{{ request.path }}"/>
|
||||||
|
<input type="hidden" name="actor" value="{{ item.meta.actor_id }}"/>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" class="bar-item">follow back</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div style="height: 100px;">
|
<div style="height: 100px;">
|
||||||
{{ utils.display_actor_inline(item.meta.actor, size=50) }}
|
{{ utils.display_actor_inline(item.meta.actor, size=50) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% elif item | has_type('Accept') %}
|
{% elif item | has_type('Accept') %}
|
||||||
<p style="margin-left:70px;padding-bottom:5px;display:inline-block;"><span class="bar-item-no-hover">you started following</span></p>
|
<div style="margin-left:70px;padding-bottom:5px;margin-bottom:15px;display:inline-block;">
|
||||||
|
{% if item.meta.notification_unread %}<span class="bar-item-no-bg"><span class="pcolor">new</span></span>{% endif %}
|
||||||
|
<span class="bar-item-no-bg">you started following</span>
|
||||||
|
{% if item.meta.notification_follows_back %}<span class="bar-item-no-hover">follows you back</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="height: 100px;">
|
<div style="height: 100px;">
|
||||||
{{ utils.display_actor_inline(item.meta.actor, size=50) }}
|
{{ utils.display_actor_inline(item.meta.actor, size=50) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
from little_boxes import activitypub as ap
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -25,3 +26,7 @@ def parse_datetime(s: str) -> datetime:
|
||||||
dt = dt.replace(tzinfo=timezone.utc)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
return dt
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
def now() -> str:
|
||||||
|
ap.format_datetime(datetime.now(timezone.utc))
|
||||||
|
|
50
utils/meta.py
Normal file
50
utils/meta.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from little_boxes import activitypub as ap
|
||||||
|
|
||||||
|
_SubQuery = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class Box(Enum):
|
||||||
|
INBOX = "inbox"
|
||||||
|
OUTBOX = "outbox"
|
||||||
|
REPLIES = "replies"
|
||||||
|
|
||||||
|
|
||||||
|
class MetaKey(Enum):
|
||||||
|
NOTIFICATION = "notification"
|
||||||
|
NOTIFICATION_UNREAD = "notification_unread"
|
||||||
|
NOTIFICATION_FOLLOWS_BACK = "notification_follows_back"
|
||||||
|
ACTOR_ID = "actor_id"
|
||||||
|
UNDO = "undo"
|
||||||
|
PUBLISHED = "published"
|
||||||
|
|
||||||
|
|
||||||
|
def _meta(mk: MetaKey) -> str:
|
||||||
|
return f"meta.{mk.value}"
|
||||||
|
|
||||||
|
|
||||||
|
def by_remote_id(remote_id: str) -> _SubQuery:
|
||||||
|
return {"remote_id": remote_id}
|
||||||
|
|
||||||
|
|
||||||
|
def in_inbox() -> _SubQuery:
|
||||||
|
return {"box": Box.INBOX.value}
|
||||||
|
|
||||||
|
|
||||||
|
def in_outbox() -> _SubQuery:
|
||||||
|
return {"box": Box.OUTBOX.value}
|
||||||
|
|
||||||
|
|
||||||
|
def by_type(type_: ap.ActivityType) -> _SubQuery:
|
||||||
|
return {"type": type_.value}
|
||||||
|
|
||||||
|
|
||||||
|
def not_undo() -> _SubQuery:
|
||||||
|
return {_meta(MetaKey.UNDO): False}
|
||||||
|
|
||||||
|
|
||||||
|
def by_actor(actor: ap.BaseActivity) -> _SubQuery:
|
||||||
|
return {_meta(MetaKey.ACTOR_ID): actor.id}
|
84
utils/notifications.py
Normal file
84
utils/notifications.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import logging
|
||||||
|
from functools import singledispatch
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from little_boxes import activitypub as ap
|
||||||
|
|
||||||
|
from config import DB
|
||||||
|
from config import MetaKey
|
||||||
|
from config import _meta
|
||||||
|
from utils.meta import by_actor
|
||||||
|
from utils.meta import by_type
|
||||||
|
from utils.meta import in_inbox
|
||||||
|
from utils.meta import not_undo
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_NewMeta = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def set_inbox_flags(activity: ap.BaseActivity, new_meta: _NewMeta) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@set_inbox_flags.register
|
||||||
|
def _accept_set_inbox_flags(activity: ap.Accept, new_meta: _NewMeta) -> None:
|
||||||
|
"""Handle notifications for "accepted" following requests."""
|
||||||
|
_logger.info(f"set_inbox_flags activity={activity!r}")
|
||||||
|
# Check if this actor already follow us back
|
||||||
|
follows_back = False
|
||||||
|
follow_query = {
|
||||||
|
**in_inbox(),
|
||||||
|
**by_type(ap.ActivityType.FOLLOW),
|
||||||
|
**by_actor(activity.get_actor()),
|
||||||
|
**not_undo(),
|
||||||
|
}
|
||||||
|
raw_follow = DB.activities.find_one(follow_query)
|
||||||
|
if raw_follow:
|
||||||
|
follows_back = True
|
||||||
|
|
||||||
|
DB.activities.update_many(
|
||||||
|
follow_query, {"$set": {_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# This Accept will be a "You started following $actor" notification
|
||||||
|
new_meta.update(
|
||||||
|
**{
|
||||||
|
_meta(MetaKey.NOTIFICATION): True,
|
||||||
|
_meta(MetaKey.NOTIFICATION_UNREAD): True,
|
||||||
|
_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): follows_back,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@set_inbox_flags.register
|
||||||
|
def _follow_set_inbox_flags(activity: ap.Follow, new_meta: _NewMeta) -> None:
|
||||||
|
"""Handle notification for new followers."""
|
||||||
|
# Check if we're already following this actor
|
||||||
|
follows_back = False
|
||||||
|
accept_query = {
|
||||||
|
**in_inbox(),
|
||||||
|
**by_type(ap.ActivityType.ACCEPT),
|
||||||
|
**by_actor(activity.get_actor()),
|
||||||
|
**not_undo(),
|
||||||
|
}
|
||||||
|
raw_accept = DB.activities.find_one(accept_query)
|
||||||
|
if raw_accept:
|
||||||
|
follows_back = True
|
||||||
|
|
||||||
|
DB.activities.update_many(
|
||||||
|
accept_query, {"$set": {_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# This Follow will be a "$actor started following you" notification
|
||||||
|
new_meta.update(
|
||||||
|
**{
|
||||||
|
_meta(MetaKey.NOTIFICATION): True,
|
||||||
|
_meta(MetaKey.NOTIFICATION_UNREAD): True,
|
||||||
|
_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): follows_back,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return None
|
Loading…
Reference in a new issue