HUGE cleanup

This commit is contained in:
Thomas Sileo 2018-06-29 22:16:26 +02:00
parent 0c2030605d
commit eb4d795ede
7 changed files with 370 additions and 382 deletions

View file

@ -1,6 +1,7 @@
import logging import logging
import os import os
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
@ -17,12 +18,12 @@ from config import ID
from config import ME from config import ME
from config import USER_AGENT from config import USER_AGENT
from config import USERNAME from config import USERNAME
from little_boxes import strtobool
from little_boxes import activitypub as ap from little_boxes import activitypub as ap
from little_boxes import strtobool
from little_boxes.activitypub import _to_list
from little_boxes.backend import Backend from little_boxes.backend import Backend
from little_boxes.collection import parse_collection as ap_parse_collection from little_boxes.collection import parse_collection as ap_parse_collection
from little_boxes.errors import Error from little_boxes.errors import Error
from little_boxes.activitypub import _to_list
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,6 +47,12 @@ def ensure_it_is_me(f):
return wrapper return wrapper
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."""
@ -68,10 +75,11 @@ class MicroblogPubBackend(Backend):
"""URL for activity link.""" """URL for activity link."""
return f"{BASE_URL}/note/{obj_id}" return f"{BASE_URL}/note/{obj_id}"
@ensure_it_is_me def save(self, box: Box, activity: ap.BaseActivity) -> None:
def outbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None: """Custom helper for saving an activity to the DB."""
DB.outbox.insert_one( DB.activities.insert_one(
{ {
"box": box.value,
"activity": activity.to_dict(), "activity": activity.to_dict(),
"type": _to_list(activity.type), "type": _to_list(activity.type),
"remote_id": activity.id, "remote_id": activity.id,
@ -79,11 +87,16 @@ class MicroblogPubBackend(Backend):
} }
) )
@ensure_it_is_me
def outbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None:
self.save(Box.OUTBOX, activity)
@ensure_it_is_me @ensure_it_is_me
def outbox_is_blocked(self, as_actor: ap.Person, actor_id: str) -> bool: def outbox_is_blocked(self, as_actor: ap.Person, actor_id: str) -> bool:
return bool( return bool(
DB.outbox.find_one( DB.activities.find_one(
{ {
"box": Box.OUTBOX.value,
"type": ap.ActivityType.BLOCK.value, "type": ap.ActivityType.BLOCK.value,
"activity.object": actor_id, "activity.object": actor_id,
"meta.undo": False, "meta.undo": False,
@ -101,14 +114,14 @@ class MicroblogPubBackend(Backend):
if iri.endswith("/activity"): if iri.endswith("/activity"):
iri = iri.replace("/activity", "") iri = iri.replace("/activity", "")
is_a_note = True is_a_note = True
data = DB.outbox.find_one({"remote_id": iri}) data = DB.activities.find_one({"box": Box.OUTBOX.value, "remote_id": iri})
if data: if data and is_a_note:
if is_a_note: return data["activity"]["object"]
return data["activity"]["object"] elif data:
return data["activity"] return data["activity"]
else: else:
# Check if the activity is stored in the inbox # Check if the activity is stored in the inbox
data = DB.inbox.find_one({"remote_id": iri}) data = DB.activities.find_one({"remote_id": iri})
if data: if data:
return data["activity"] return data["activity"]
@ -117,18 +130,11 @@ class MicroblogPubBackend(Backend):
@ensure_it_is_me @ensure_it_is_me
def inbox_check_duplicate(self, as_actor: ap.Person, iri: str) -> bool: def inbox_check_duplicate(self, as_actor: ap.Person, iri: str) -> bool:
return bool(DB.inbox.find_one({"remote_id": iri})) return bool(DB.activities.find_one({"box": Box.INBOX.value, "remote_id": iri}))
@ensure_it_is_me @ensure_it_is_me
def inbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None: def inbox_new(self, as_actor: ap.Person, activity: ap.BaseActivity) -> None:
DB.inbox.insert_one( self.save(Box.INBOX, activity)
{
"activity": activity.to_dict(),
"type": _to_list(activity.type),
"remote_id": activity.id,
"meta": {"undo": False, "deleted": False},
}
)
@ensure_it_is_me @ensure_it_is_me
def post_to_remote_inbox(self, as_actor: ap.Person, payload: str, to: str) -> None: def post_to_remote_inbox(self, as_actor: ap.Person, payload: str, to: str) -> None:
@ -161,41 +167,37 @@ class MicroblogPubBackend(Backend):
def inbox_like(self, as_actor: ap.Person, like: ap.Like) -> None: def inbox_like(self, as_actor: ap.Person, like: ap.Like) -> None:
obj = like.get_object() obj = like.get_object()
# Update the meta counter if the object is published by the server # Update the meta counter if the object is published by the server
DB.outbox.update_one( DB.activities.update_one(
{"activity.object.id": obj.id}, {"$inc": {"meta.count_like": 1}} {"box": Box.OUTBOX.value, "activity.object.id": obj.id},
{"$inc": {"meta.count_like": 1}},
) )
@ensure_it_is_me @ensure_it_is_me
def inbox_undo_like(self, as_actor: ap.Person, like: ap.Like) -> None: def inbox_undo_like(self, as_actor: ap.Person, like: ap.Like) -> None:
obj = like.get_object() obj = like.get_object()
# Update the meta counter if the object is published by the server # Update the meta counter if the object is published by the server
DB.outbox.update_one( DB.activities.update_one(
{"activity.object.id": obj.id}, {"$inc": {"meta.count_like": -1}} {"box": Box.OUTBOX.value, "activity.object.id": obj.id},
{"$inc": {"meta.count_like": -1}},
) )
@ensure_it_is_me @ensure_it_is_me
def outbox_like(self, as_actor: ap.Person, like: ap.Like) -> None: def outbox_like(self, as_actor: ap.Person, like: ap.Like) -> None:
obj = like.get_object() obj = like.get_object()
# Unlikely, but an actor can like it's own post DB.activities.update_one(
DB.outbox.update_one( {"activity.object.id": obj.id},
{"activity.object.id": obj.id}, {"$inc": {"meta.count_like": 1}} {"$inc": {"meta.count_like": 1}, "$set": {"meta.liked": like.id}},
) )
DB.activities.update_one(
# Keep track of the like we just performed {"remote_id": like.id}, {"$set": {"meta.object": obj.to_dict(embed=True)}}
DB.inbox.update_one(
{"activity.object.id": obj.id}, {"$set": {"meta.liked": like.id}}
) )
@ensure_it_is_me @ensure_it_is_me
def outbox_undo_like(self, as_actor: ap.Person, like: ap.Like) -> None: def outbox_undo_like(self, as_actor: ap.Person, like: ap.Like) -> None:
obj = like.get_object() obj = like.get_object()
# Unlikely, but an actor can like it's own post DB.activities.update_one(
DB.outbox.update_one( {"activity.object.id": obj.id},
{"activity.object.id": obj.id}, {"$inc": {"meta.count_like": -1}} {"$inc": {"meta.count_like": -1}, "$set": {"meta.liked": False}},
)
DB.inbox.update_one(
{"activity.object.id": obj.id}, {"$set": {"meta.liked": False}}
) )
@ensure_it_is_me @ensure_it_is_me
@ -204,57 +206,57 @@ class MicroblogPubBackend(Backend):
"object" "object"
].startswith("http"): ].startswith("http"):
# TODO(tsileo): actually drop it without storing it and better logging, also move the check somewhere else # TODO(tsileo): actually drop it without storing it and better logging, also move the check somewhere else
# or remote it?
logger.warn( logger.warn(
f'received an Annouce referencing an OStatus notice ({announce._data["object"]}), dropping the message' f'received an Annouce referencing an OStatus notice ({announce._data["object"]}), dropping the message'
) )
return return
# FIXME(tsileo): Save/cache the object, and make it part of the stream so we can fetch it
if isinstance(announce._data["object"], str):
obj_iri = announce._data["object"]
else:
obj_iri = self.get_object().id
DB.outbox.update_one( obj = announce.get_object()
{"activity.object.id": obj_iri}, {"$inc": {"meta.count_boost": 1}} DB.activities.update_one(
{"remote_id": announce.id},
{"$set": {"meta.object": obj.to_dict(embed=True)}},
)
DB.activities.update_one(
{"activity.object.id": obj.id}, {"$inc": {"meta.count_boost": 1}}
) )
@ensure_it_is_me @ensure_it_is_me
def inbox_undo_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: def inbox_undo_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None:
obj = announce.get_object() obj = announce.get_object()
# Update the meta counter if the object is published by the server # Update the meta counter if the object is published by the server
DB.outbox.update_one( DB.activities.update_one(
{"activity.object.id": obj.id}, {"$inc": {"meta.count_boost": -1}} {"activity.object.id": obj.id}, {"$inc": {"meta.count_boost": -1}}
) )
@ensure_it_is_me @ensure_it_is_me
def outbox_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: def outbox_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None:
obj = announce.get_object() obj = announce.get_object()
DB.inbox.update_one( DB.activities.update_one(
{"remote_id": announce.id},
{"$set": {"meta.object": obj.to_dict(embed=True)}},
)
DB.activities.update_one(
{"activity.object.id": obj.id}, {"$set": {"meta.boosted": announce.id}} {"activity.object.id": obj.id}, {"$set": {"meta.boosted": announce.id}}
) )
@ensure_it_is_me @ensure_it_is_me
def outbox_undo_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None: def outbox_undo_announce(self, as_actor: ap.Person, announce: ap.Announce) -> None:
obj = announce.get_object() obj = announce.get_object()
DB.inbox.update_one( DB.activities.update_one(
{"activity.object.id": obj.id}, {"$set": {"meta.boosted": False}} {"activity.object.id": obj.id}, {"$set": {"meta.boosted": False}}
) )
@ensure_it_is_me @ensure_it_is_me
def inbox_delete(self, as_actor: ap.Person, delete: ap.Delete) -> None: def inbox_delete(self, as_actor: ap.Person, delete: ap.Delete) -> None:
if not DB.inbox.find_one_and_update(
{"activity.object.id": delete.get_object().id},
{"$set": {"meta.deleted": True}},
):
DB.threads.update_one(
{"activity.object.id": delete.get_object().id},
{"$set": {"meta.deleted": True}},
)
obj = delete.get_object() obj = delete.get_object()
DB.activities.update_one(
{"activity.object.id": obj.id}, {"$set": {"meta.deleted": True}}
)
if obj.ACTIVITY_TYPE != ap.ActivityType.NOTE: if obj.ACTIVITY_TYPE != ap.ActivityType.NOTE:
obj = ap.parse_activity( obj = ap.parse_activity(
DB.inbox.find_one( DB.activities.find_one(
{ {
"activity.object.id": delete.get_object().id, "activity.object.id": delete.get_object().id,
"type": ap.ActivityType.CREATE.value, "type": ap.ActivityType.CREATE.value,
@ -268,14 +270,14 @@ class MicroblogPubBackend(Backend):
@ensure_it_is_me @ensure_it_is_me
def outbox_delete(self, as_actor: ap.Person, delete: ap.Delete) -> None: def outbox_delete(self, as_actor: ap.Person, delete: ap.Delete) -> None:
DB.outbox.update_one( DB.activities.update_one(
{"activity.object.id": delete.get_object().id}, {"activity.object.id": delete.get_object().id},
{"$set": {"meta.deleted": True}}, {"$set": {"meta.deleted": True}},
) )
obj = delete.get_object() obj = delete.get_object()
if delete.get_object().ACTIVITY_TYPE != ap.ActivityType.NOTE: if delete.get_object().ACTIVITY_TYPE != ap.ActivityType.NOTE:
obj = ap.parse_activity( obj = ap.parse_activity(
DB.outbox.find_one( DB.activities.find_one(
{ {
"activity.object.id": delete.get_object().id, "activity.object.id": delete.get_object().id,
"type": ap.ActivityType.CREATE.value, "type": ap.ActivityType.CREATE.value,
@ -289,15 +291,10 @@ class MicroblogPubBackend(Backend):
def inbox_update(self, as_actor: ap.Person, update: ap.Update) -> None: def inbox_update(self, as_actor: ap.Person, update: ap.Update) -> None:
obj = update.get_object() obj = update.get_object()
if obj.ACTIVITY_TYPE == ap.ActivityType.NOTE: if obj.ACTIVITY_TYPE == ap.ActivityType.NOTE:
if not DB.inbox.find_one_and_update( DB.activities.update_one(
{"activity.object.id": obj.id}, {"activity.object.id": obj.id},
{"$set": {"activity.object": obj.to_dict()}}, {"$set": {"activity.object": obj.to_dict()}},
): )
DB.threads.update_one(
{"activity.object.id": obj.id},
{"$set": {"activity.object": obj.to_dict()}},
)
# FIXME(tsileo): handle update actor amd inbox_update_note/inbox_update_actor # FIXME(tsileo): handle update actor amd inbox_update_note/inbox_update_actor
@ensure_it_is_me @ensure_it_is_me
@ -322,7 +319,7 @@ class MicroblogPubBackend(Backend):
print(f"updating note from outbox {obj!r} {update}") print(f"updating note from outbox {obj!r} {update}")
logger.info(f"updating note from outbox {obj!r} {update}") logger.info(f"updating note from outbox {obj!r} {update}")
DB.outbox.update_one({"activity.object.id": obj["id"]}, update) DB.activities.update_one({"activity.object.id": obj["id"]}, update)
# FIXME(tsileo): should send an Update (but not a partial one, to all the note's recipients # FIXME(tsileo): should send an Update (but not a partial one, to all the note's recipients
# (create a new Update with the result of the update, and send it without saving it?) # (create a new Update with the result of the update, and send it without saving it?)
@ -340,18 +337,10 @@ class MicroblogPubBackend(Backend):
if not in_reply_to: if not in_reply_to:
pass pass
if not DB.inbox.find_one_and_update( DB.activities.update_one(
{"activity.object.id": in_reply_to}, {"activity.object.id": in_reply_to},
{"$inc": {"meta.count_reply": -1, "meta.count_direct_reply": -1}}, {"$inc": {"meta.count_reply": -1, "meta.count_direct_reply": -1}},
): )
if not DB.outbox.find_one_and_update(
{"activity.object.id": in_reply_to},
{"$inc": {"meta.count_reply": -1, "meta.count_direct_reply": -1}},
):
DB.threads.update_one(
{"activity.object.id": in_reply_to},
{"$inc": {"meta.count_reply": -1, "meta.count_direct_reply": -1}},
)
@ensure_it_is_me @ensure_it_is_me
def _handle_replies(self, as_actor: ap.Person, create: ap.Create) -> None: def _handle_replies(self, as_actor: ap.Person, create: ap.Create) -> None:
@ -365,24 +354,14 @@ class MicroblogPubBackend(Backend):
root_reply = in_reply_to root_reply = in_reply_to
reply = ap.fetch_remote_activity(root_reply, expected=ap.ActivityType.NOTE) reply = ap.fetch_remote_activity(root_reply, expected=ap.ActivityType.NOTE)
if not DB.inbox.find_one_and_update( creply = DB.activities.find_one_and_update(
{"activity.object.id": in_reply_to}, {"activity.object.id": in_reply_to},
{"$inc": {"meta.count_reply": 1, "meta.count_direct_reply": 1}}, {"$inc": {"meta.count_reply": 1, "meta.count_direct_reply": 1}},
): )
if not DB.outbox.find_one_and_update( if not creply:
{"activity.object.id": in_reply_to}, # It means the activity is not in the inbox, and not in the outbox, we want to save it
{"$inc": {"meta.count_reply": 1, "meta.count_direct_reply": 1}}, self.save(Box.REPLIES, reply)
): new_threads.append(reply.id)
# It means the activity is not in the inbox, and not in the outbox, we want to save it
DB.threads.insert_one(
{
"activity": reply.to_dict(),
"type": _to_list(reply.type),
"remote_id": reply.id,
"meta": {"undo": False, "deleted": False},
}
)
new_threads.append(reply.id)
while reply is not None: while reply is not None:
in_reply_to = reply.inReplyTo in_reply_to = reply.inReplyTo
@ -391,25 +370,15 @@ class MicroblogPubBackend(Backend):
root_reply = in_reply_to root_reply = in_reply_to
reply = ap.fetch_remote_activity(root_reply, expected=ap.ActivityType.NOTE) reply = ap.fetch_remote_activity(root_reply, expected=ap.ActivityType.NOTE)
q = {"activity.object.id": root_reply} q = {"activity.object.id": root_reply}
if not DB.inbox.count(q) and not DB.outbox.count(q): if not DB.activities.count(q):
DB.threads.insert_one( self.save(Box.REPLIES, reply)
{
"activity": reply.to_dict(),
"type": _to_list(reply.type),
"remote_id": reply.id,
"meta": {"undo": False, "deleted": False},
}
)
new_threads.append(reply.id) new_threads.append(reply.id)
q = {"remote_id": create.id} DB.activities.update_one(
if not DB.inbox.find_one_and_update( {"remote_id": create.id}, {"$set": {"meta.thread_root_parent": root_reply}}
q, {"$set": {"meta.thread_root_parent": root_reply}} )
): DB.activities.update(
DB.outbox.update_one(q, {"$set": {"meta.thread_root_parent": root_reply}}) {"box": Box.REPLIES.value, "remote_id": {"$in": new_threads}},
DB.threads.update(
{"remote_id": {"$in": new_threads}},
{"$set": {"meta.thread_root_parent": root_reply}}, {"$set": {"meta.thread_root_parent": root_reply}},
) )
@ -423,7 +392,9 @@ def gen_feed():
fg.description(f"{USERNAME} notes") fg.description(f"{USERNAME} notes")
fg.logo(ME.get("icon", {}).get("url")) fg.logo(ME.get("icon", {}).get("url"))
fg.language("en") fg.language("en")
for item in DB.outbox.find({"type": "Create"}, limit=50): for item in DB.activities.find(
{"box": Box.OUTBOX.value, "type": "Create"}, limit=50
):
fe = fg.add_entry() fe = fg.add_entry()
fe.id(item["activity"]["object"].get("url")) fe.id(item["activity"]["object"].get("url"))
fe.link(href=item["activity"]["object"].get("url")) fe.link(href=item["activity"]["object"].get("url"))
@ -435,7 +406,9 @@ def gen_feed():
def json_feed(path: str) -> Dict[str, Any]: def json_feed(path: str) -> Dict[str, Any]:
"""JSON Feed (https://jsonfeed.org/) document.""" """JSON Feed (https://jsonfeed.org/) document."""
data = [] data = []
for item in DB.outbox.find({"type": "Create"}, limit=50): for item in DB.activities.find(
{"box": Box.OUTBOX.value, "type": "Create"}, limit=50
):
data.append( data.append(
{ {
"id": item["id"], "id": item["id"],
@ -471,11 +444,15 @@ def build_inbox_json_feed(
data = [] data = []
cursor = None cursor = None
q: Dict[str, Any] = {"type": "Create", "meta.deleted": False} q: Dict[str, Any] = {
"type": "Create",
"meta.deleted": False,
"box": Box.INBOX.value,
}
if request_cursor: if request_cursor:
q["_id"] = {"$lt": request_cursor} q["_id"] = {"$lt": request_cursor}
for item in DB.inbox.find(q, limit=50).sort("_id", -1): for item in DB.activities.find(q, limit=50).sort("_id", -1):
actor = ap.get_backend().fetch_iri(item["activity"]["actor"]) actor = ap.get_backend().fetch_iri(item["activity"]["actor"])
data.append( data.append(
{ {

402
app.py
View file

@ -38,6 +38,7 @@ from werkzeug.utils import secure_filename
import activitypub import activitypub
import config import config
from activitypub import Box
from activitypub import embed_collection from activitypub import embed_collection
from config import ADMIN_API_KEY from config import ADMIN_API_KEY
from config import BASE_URL from config import BASE_URL
@ -56,6 +57,7 @@ from config import _drop_db
from config import custom_cache_purge_hook from config import custom_cache_purge_hook
from little_boxes import activitypub as ap from little_boxes import activitypub as ap
from little_boxes.activitypub import ActivityType from little_boxes.activitypub import ActivityType
from little_boxes.activitypub import _to_list
from little_boxes.activitypub import clean_activity from little_boxes.activitypub import clean_activity
from little_boxes.activitypub import get_backend from little_boxes.activitypub import get_backend
from little_boxes.content_helper import parse_markdown from little_boxes.content_helper import parse_markdown
@ -111,18 +113,21 @@ def inject_config():
"activity.object.inReplyTo": None, "activity.object.inReplyTo": None,
"meta.deleted": False, "meta.deleted": False,
} }
notes_count = DB.outbox.find( notes_count = DB.activities.find(
{"$or": [q, {"type": "Announce", "meta.undo": False}]} {"box": Box.OUTBOX.value, "$or": [q, {"type": "Announce", "meta.undo": False}]}
).count() ).count()
q = {"type": "Create", "activity.object.type": "Note", "meta.deleted": False} q = {"type": "Create", "activity.object.type": "Note", "meta.deleted": False}
with_replies_count = DB.outbox.find( with_replies_count = DB.activities.find(
{"$or": [q, {"type": "Announce", "meta.undo": False}]} {"box": Box.OUTBOX.value, "$or": [q, {"type": "Announce", "meta.undo": False}]}
).count() ).count()
liked_count = DB.outbox.count({ liked_count = DB.activities.count(
"meta.deleted": False, {
"meta.undo": False, "box": Box.OUTBOX.value,
"type": ActivityType.LIKE.value, "meta.deleted": False,
}) "meta.undo": False,
"type": ActivityType.LIKE.value,
}
)
return dict( return dict(
microblogpub_version=VERSION, microblogpub_version=VERSION,
config=config, config=config,
@ -132,6 +137,7 @@ def inject_config():
notes_count=notes_count, notes_count=notes_count,
liked_count=liked_count, liked_count=liked_count,
with_replies_count=with_replies_count, with_replies_count=with_replies_count,
me=ME,
) )
@ -172,6 +178,11 @@ def clean_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS) return bleach.clean(html, tags=ALLOWED_TAGS)
@app.template_filter()
def permalink_id(val):
return str(hash(val))
@app.template_filter() @app.template_filter()
def quote_plus(t): def quote_plus(t):
return urllib.parse.quote_plus(t) return urllib.parse.quote_plus(t)
@ -221,6 +232,13 @@ def format_timeago(val):
return val return val
@app.template_filter()
def has_type(doc, _type):
if _type in _to_list(doc["type"]):
return True
return False
def _is_img(filename): def _is_img(filename):
filename = filename.lower() filename = filename.lower()
if ( if (
@ -370,9 +388,7 @@ def login():
payload = u2f.begin_authentication(ID, devices) payload = u2f.begin_authentication(ID, devices)
session["challenge"] = payload session["challenge"] = payload
return render_template( return render_template("login.html", u2f_enabled=u2f_enabled, payload=payload)
"login.html", u2f_enabled=u2f_enabled, me=ME, payload=payload
)
@app.route("/remote_follow", methods=["GET", "POST"]) @app.route("/remote_follow", methods=["GET", "POST"])
@ -429,6 +445,45 @@ def u2f_register():
# Activity pub routes # Activity pub routes
@app.route("/migration1_step1")
@login_required
def tmp_migrate():
for activity in DB.outbox.find():
activity["box"] = Box.OUTBOX.value
DB.activities.insert_one(activity)
for activity in DB.inbox.find():
activity["box"] = Box.INBOX.value
DB.activities.insert_one(activity)
for activity in DB.replies.find():
activity["box"] = Box.REPLIES.value
DB.activities.insert_one(activity)
return "Done"
@app.route("/migration1_step2")
@login_required
def tmp_migrate2():
for activity in DB.activities.find():
if (
activity["box"] == Box.OUTBOX.value
and activity["type"] == ActivityType.LIKE.value
):
like = ap.parse_activity(activity["activity"])
obj = like.get_object()
DB.activities.update_one(
{"remote_id": like.id},
{"$set": {"meta.object": obj.to_dict(embed=True)}},
)
elif activity["type"] == ActivityType.ANNOUNCE.value:
announce = ap.parse_activity(activity["activity"])
obj = announce.get_object()
DB.activities.update_one(
{"remote_id": announce.id},
{"$set": {"meta.object": obj.to_dict(embed=True)}},
)
return "Done"
@app.route("/") @app.route("/")
def index(): def index():
if is_api_request(): if is_api_request():
@ -437,89 +492,44 @@ def index():
# FIXME(tsileo): implements pagination, also for the followers/following page # FIXME(tsileo): implements pagination, also for the followers/following page
limit = 50 limit = 50
q = { q = {
"type": "Create", "box": Box.OUTBOX.value,
"activity.object.type": "Note", "type": {"$in": [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]},
"activity.object.inReplyTo": None, "activity.object.inReplyTo": None,
"meta.deleted": False, "meta.deleted": False,
"meta.undo": False,
} }
c = request.args.get("cursor") c = request.args.get("cursor")
if c: if c:
q["_id"] = {"$lt": ObjectId(c)} q["_id"] = {"$lt": ObjectId(c)}
outbox_data = list( outbox_data = list(DB.activities.find(q, limit=limit).sort("_id", -1))
DB.outbox.find(
{"$or": [q, {"type": "Announce", "meta.undo": False}]}, limit=limit
).sort("_id", -1)
)
cursor = None cursor = None
if outbox_data and len(outbox_data) == limit: if outbox_data and len(outbox_data) == limit:
cursor = str(outbox_data[-1]["_id"]) cursor = str(outbox_data[-1]["_id"])
for data in outbox_data: return render_template("index.html", outbox_data=outbox_data, cursor=cursor)
if data["type"] == "Announce":
if data["activity"]["object"].startswith("http"):
data["ref"] = {
"activity": {
"object": OBJECT_SERVICE.get(data["activity"]["object"]),
"id": "NA",
},
"meta": {},
}
print(data)
return render_template(
"index.html",
me=ME,
notes=DB.inbox.find(
{"type": "Create", "activity.object.type": "Note", "meta.deleted": False}
).count(),
followers=DB.followers.count(),
following=DB.following.count(),
outbox_data=outbox_data,
cursor=cursor,
)
@app.route("/with_replies") @app.route("/with_replies")
def with_replies(): def with_replies():
# FIXME(tsileo): implements pagination, also for the followers/following page
limit = 50 limit = 50
q = {"type": "Create", "activity.object.type": "Note", "meta.deleted": False} q = {
"box": Box.OUTBOX.value,
"type": {"$in": [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]},
"meta.deleted": False,
"meta.undo": False,
}
c = request.args.get("cursor") c = request.args.get("cursor")
if c: if c:
q["_id"] = {"$lt": ObjectId(c)} q["_id"] = {"$lt": ObjectId(c)}
outbox_data = list( outbox_data = list(DB.activities.find(q, limit=limit).sort("_id", -1))
DB.outbox.find(
{"$or": [q, {"type": "Announce", "meta.undo": False}]}, limit=limit
).sort("_id", -1)
)
cursor = None cursor = None
if outbox_data and len(outbox_data) == limit: if outbox_data and len(outbox_data) == limit:
cursor = str(outbox_data[-1]["_id"]) cursor = str(outbox_data[-1]["_id"])
for data in outbox_data: return render_template("index.html", outbox_data=outbox_data, cursor=cursor)
if data["type"] == "Announce":
print(data)
if data["activity"]["object"].startswith("http"):
data["ref"] = {
"activity": {
"object": OBJECT_SERVICE.get(data["activity"]["object"])
},
"meta": {},
}
return render_template(
"index.html",
me=ME,
notes=DB.inbox.find(
{"type": "Create", "activity.object.type": "Note", "meta.deleted": False}
).count(),
followers=DB.followers.count(),
following=DB.following.count(),
outbox_data=outbox_data,
cursor=cursor,
)
def _build_thread(data, include_children=True): def _build_thread(data, include_children=True):
@ -534,12 +544,7 @@ def _build_thread(data, include_children=True):
) )
# Fetch the root replies, and the children # Fetch the root replies, and the children
replies = ( replies = [data] + list(DB.activities.find(query))
[data]
+ list(DB.inbox.find(query))
+ list(DB.outbox.find(query))
+ list(DB.threads.find(query))
)
replies = sorted(replies, key=lambda d: d["activity"]["object"]["published"]) replies = sorted(replies, key=lambda d: d["activity"]["object"]["published"])
# Index all the IDs in order to build a tree # Index all the IDs in order to build a tree
idx = {} idx = {}
@ -580,7 +585,9 @@ def _build_thread(data, include_children=True):
@app.route("/note/<note_id>") @app.route("/note/<note_id>")
def note_by_id(note_id): def note_by_id(note_id):
data = DB.outbox.find_one({"remote_id": back.activity_url(note_id)}) data = DB.activities.find_one(
{"box": Box.OUTBOX.value, "remote_id": back.activity_url(note_id)}
)
if not data: if not data:
abort(404) abort(404)
if data["meta"].get("deleted", False): if data["meta"].get("deleted", False):
@ -588,7 +595,7 @@ def note_by_id(note_id):
thread = _build_thread(data) thread = _build_thread(data)
likes = list( likes = list(
DB.inbox.find( DB.activities.find(
{ {
"meta.undo": False, "meta.undo": False,
"type": ActivityType.LIKE.value, "type": ActivityType.LIKE.value,
@ -602,7 +609,7 @@ def note_by_id(note_id):
likes = [ACTOR_SERVICE.get(doc["activity"]["actor"]) for doc in likes] likes = [ACTOR_SERVICE.get(doc["activity"]["actor"]) for doc in likes]
shares = list( shares = list(
DB.inbox.find( DB.activities.find(
{ {
"meta.undo": False, "meta.undo": False,
"type": ActivityType.ANNOUNCE.value, "type": ActivityType.ANNOUNCE.value,
@ -616,7 +623,7 @@ def note_by_id(note_id):
shares = [ACTOR_SERVICE.get(doc["activity"]["actor"]) for doc in shares] shares = [ACTOR_SERVICE.get(doc["activity"]["actor"]) for doc in shares]
return render_template( return render_template(
"note.html", likes=likes, shares=shares, me=ME, thread=thread, note=data "note.html", likes=likes, shares=shares, thread=thread, note=data
) )
@ -636,7 +643,10 @@ def nodeinfo():
"protocols": ["activitypub"], "protocols": ["activitypub"],
"services": {"inbound": [], "outbound": []}, "services": {"inbound": [], "outbound": []},
"openRegistrations": False, "openRegistrations": False,
"usage": {"users": {"total": 1}, "localPosts": DB.outbox.count()}, "usage": {
"users": {"total": 1},
"localPosts": DB.activities.count({"box": Box.OUTBOX.value}),
},
"metadata": { "metadata": {
"sourceCode": "https://github.com/tsileo/microblog.pub", "sourceCode": "https://github.com/tsileo/microblog.pub",
"nodeName": f"@{USERNAME}@{DOMAIN}", "nodeName": f"@{USERNAME}@{DOMAIN}",
@ -734,12 +744,13 @@ def outbox():
# TODO(tsileo): filter the outbox if not authenticated # TODO(tsileo): filter the outbox if not authenticated
# FIXME(tsileo): filter deleted, add query support for build_ordered_collection # FIXME(tsileo): filter deleted, add query support for build_ordered_collection
q = { q = {
"box": Box.OUTBOX.value,
"meta.deleted": False, "meta.deleted": False,
# 'type': {'$in': [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]}, # 'type': {'$in': [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]},
} }
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.outbox, DB.activities,
q=q, q=q,
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: activity_from_doc(doc, embed=True), map_func=lambda doc: activity_from_doc(doc, embed=True),
@ -765,7 +776,9 @@ def outbox():
@app.route("/outbox/<item_id>") @app.route("/outbox/<item_id>")
def outbox_detail(item_id): def outbox_detail(item_id):
doc = DB.outbox.find_one({"remote_id": back.activity_url(item_id)}) doc = DB.activities.find_one(
{"box": Box.OUTBOX.value, "remote_id": back.activity_url(item_id)}
)
if doc["meta"].get("deleted", False): if doc["meta"].get("deleted", False):
obj = ap.parse_activity(doc["activity"]) obj = ap.parse_activity(doc["activity"])
resp = jsonify(**obj.get_object().get_tombstone()) resp = jsonify(**obj.get_object().get_tombstone())
@ -777,8 +790,12 @@ def outbox_detail(item_id):
@app.route("/outbox/<item_id>/activity") @app.route("/outbox/<item_id>/activity")
def outbox_activity(item_id): def outbox_activity(item_id):
# TODO(tsileo): handle Tombstone # TODO(tsileo): handle Tombstone
data = DB.outbox.find_one( data = DB.activities.find_one(
{"remote_id": back.activity_url(item_id), "meta.deleted": False} {
"box": Box.OUTBOX.value,
"remote_id": back.activity_url(item_id),
"meta.deleted": False,
}
) )
if not data: if not data:
abort(404) abort(404)
@ -793,8 +810,12 @@ def outbox_activity_replies(item_id):
# TODO(tsileo): handle Tombstone # TODO(tsileo): handle Tombstone
if not is_api_request(): if not is_api_request():
abort(404) abort(404)
data = DB.outbox.find_one( data = DB.activities.find_one(
{"remote_id": back.activity_url(item_id), "meta.deleted": False} {
"box": Box.OUTBOX.value,
"remote_id": back.activity_url(item_id),
"meta.deleted": False,
}
) )
if not data: if not data:
abort(404) abort(404)
@ -810,7 +831,7 @@ def outbox_activity_replies(item_id):
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.inbox, DB.activities,
q=q, q=q,
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: doc["activity"]["object"], map_func=lambda doc: doc["activity"]["object"],
@ -825,8 +846,12 @@ def outbox_activity_likes(item_id):
# TODO(tsileo): handle Tombstone # TODO(tsileo): handle Tombstone
if not is_api_request(): if not is_api_request():
abort(404) abort(404)
data = DB.outbox.find_one( data = DB.activities.find_one(
{"remote_id": back.activity_url(item_id), "meta.deleted": False} {
"box": Box.OUTBOX.value,
"remote_id": back.activity_url(item_id),
"meta.deleted": False,
}
) )
if not data: if not data:
abort(404) abort(404)
@ -845,7 +870,7 @@ def outbox_activity_likes(item_id):
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.inbox, DB.activities,
q=q, q=q,
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: remove_context(doc["activity"]), map_func=lambda doc: remove_context(doc["activity"]),
@ -860,8 +885,12 @@ def outbox_activity_shares(item_id):
# TODO(tsileo): handle Tombstone # TODO(tsileo): handle Tombstone
if not is_api_request(): if not is_api_request():
abort(404) abort(404)
data = DB.outbox.find_one( data = DB.activities.find_one(
{"remote_id": back.activity_url(item_id), "meta.deleted": False} {
"box": Box.OUTBOX.value,
"remote_id": back.activity_url(item_id),
"meta.deleted": False,
}
) )
if not data: if not data:
abort(404) abort(404)
@ -880,7 +909,7 @@ def outbox_activity_shares(item_id):
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.inbox, DB.activities,
q=q, q=q,
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: remove_context(doc["activity"]), map_func=lambda doc: remove_context(doc["activity"]),
@ -893,16 +922,21 @@ def outbox_activity_shares(item_id):
@app.route("/admin", methods=["GET"]) @app.route("/admin", methods=["GET"])
@login_required @login_required
def admin(): def admin():
q = {"meta.deleted": False, "meta.undo": False, "type": ActivityType.LIKE.value} q = {
col_liked = DB.outbox.count(q) "meta.deleted": False,
"meta.undo": False,
"type": ActivityType.LIKE.value,
"box": Box.OUTBOX.value,
}
col_liked = DB.activities.count(q)
return render_template( return render_template(
"admin.html", "admin.html",
instances=list(DB.instances.find()), instances=list(DB.instances.find()),
inbox_size=DB.inbox.count(), inbox_size=DB.activities.count({"box": Box.INBOX.value}),
outbox_size=DB.outbox.count(), outbox_size=DB.activities.count({"box": Box.OUTBOX.value}),
object_cache_size=DB.objects_cache.count(), object_cache_size=0,
actor_cache_size=DB.actors_cache.count(), actor_cache_size=0,
col_liked=col_liked, col_liked=col_liked,
col_followers=DB.followers.count(), col_followers=DB.followers.count(),
col_following=DB.following.count(), col_following=DB.following.count(),
@ -916,11 +950,9 @@ def new():
content = "" content = ""
thread = [] thread = []
if request.args.get("reply"): if request.args.get("reply"):
data = DB.inbox.find_one({"activity.object.id": request.args.get("reply")}) data = DB.activities.find_one({"activity.object.id": request.args.get("reply")})
if not data: if not data:
data = DB.outbox.find_one({"activity.object.id": request.args.get("reply")}) abort(400)
if not data:
abort(400)
reply = ap.parse_activity(data["activity"]) reply = ap.parse_activity(data["activity"])
reply_id = reply.id reply_id = reply.id
@ -930,7 +962,7 @@ def new():
domain = urlparse(actor.id).netloc domain = urlparse(actor.id).netloc
# FIXME(tsileo): if reply of reply, fetch all participants # FIXME(tsileo): if reply of reply, fetch all participants
content = f"@{actor.preferredUsername}@{domain} " content = f"@{actor.preferredUsername}@{domain} "
thread = _build_thread(data, include_children=False) thread = _build_thread(data)
return render_template("new.html", reply=reply_id, content=content, thread=thread) return render_template("new.html", reply=reply_id, content=content, thread=thread)
@ -940,43 +972,42 @@ def new():
def notifications(): def notifications():
# FIXME(tsileo): implements pagination, also for the followers/following page # FIXME(tsileo): implements pagination, also for the followers/following page
limit = 50 limit = 50
q = { # FIXME(tsileo): show unfollow (performed by the current actor) and liked???
"type": "Create", mentions_query = {
"type": ActivityType.CREATE.value,
"activity.object.tag.type": "Mention", "activity.object.tag.type": "Mention",
"activity.object.tag.name": f"@{USERNAME}@{DOMAIN}", "activity.object.tag.name": f"@{USERNAME}@{DOMAIN}",
"meta.deleted": False, "meta.deleted": False,
} }
# TODO(tsileo): also include replies via regex on Create replyTo replies_query = {
q = { "type": ActivityType.CREATE.value,
"$or": [ "activity.object.inReplyTo": {"$regex": f"^{BASE_URL}"},
q, }
{"type": "Follow"}, announced_query = {
{"type": "Accept"}, "type": ActivityType.ANNOUNCE.value,
{"type": "Undo", "activity.object.type": "Follow"}, "activity.object": {"$regex": f"^{BASE_URL}"},
{"type": "Announce", "activity.object": {"$regex": f"^{BASE_URL}"}}, }
{"type": "Create", "activity.object.inReplyTo": {"$regex": f"^{BASE_URL}"}}, new_followers_query = {"type": ActivityType.FOLLOW.value}
] followed_query = {"type": ActivityType.ACCEPT.value}
q = {
"box": Box.INBOX.value,
"$or": [
mentions_query,
announced_query,
replies_query,
new_followers_query,
followed_query,
],
} }
print(q)
c = request.args.get("cursor") c = request.args.get("cursor")
if c: if c:
q["_id"] = {"$lt": ObjectId(c)} q["_id"] = {"$lt": ObjectId(c)}
outbox_data = list(DB.inbox.find(q, limit=limit).sort("_id", -1)) outbox_data = list(DB.activities.find(q, limit=limit).sort("_id", -1))
cursor = None cursor = None
if outbox_data and len(outbox_data) == limit: if outbox_data and len(outbox_data) == limit:
cursor = str(outbox_data[-1]["_id"]) cursor = str(outbox_data[-1]["_id"])
# TODO(tsileo): fix the annonce handling, copy it from /stream
# for data in outbox_data:
# if data['type'] == 'Announce':
# print(data)
# if data['activity']['object'].startswith('http') and data['activity']['object'] in objcache:
# data['ref'] = {'activity': {'object': objcache[data['activity']['object']]}, 'meta': {}}
# out.append(data)
# else:
# out.append(data)
return render_template("stream.html", inbox_data=outbox_data, cursor=cursor) return render_template("stream.html", inbox_data=outbox_data, cursor=cursor)
@ -1061,8 +1092,11 @@ def api_like():
@api_required @api_required
def api_undo(): def api_undo():
oid = _user_api_arg("id") oid = _user_api_arg("id")
doc = DB.outbox.find_one( doc = DB.activities.find_one(
{"$or": [{"remote_id": back.activity_url(oid)}, {"remote_id": oid}]} {
"box": Box.OUTBOX.value,
"$or": [{"remote_id": back.activity_url(oid)}, {"remote_id": oid}],
}
) )
if not doc: if not doc:
raise ActivityNotFoundError(f"cannot found {oid}") raise ActivityNotFoundError(f"cannot found {oid}")
@ -1080,50 +1114,24 @@ def api_undo():
def stream(): def stream():
# FIXME(tsileo): implements pagination, also for the followers/following page # FIXME(tsileo): implements pagination, also for the followers/following page
limit = 100 limit = 100
c = request.args.get("cursor")
q = { q = {
"type": "Create", "box": Box.INBOX.value,
"activity.object.type": "Note", "type": {"$in": [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]},
"activity.object.inReplyTo": None,
"meta.deleted": False, "meta.deleted": False,
} }
c = request.args.get("cursor")
if c: if c:
q["_id"] = {"$lt": ObjectId(c)} q["_id"] = {"$lt": ObjectId(c)}
outbox_data = list( inbox_data = list(
DB.inbox.find({"$or": [q, {"type": "Announce"}]}, limit=limit).sort( # FIXME(tsileo): reshape using meta.cached_object
"activity.published", -1 DB.activities.find(q, limit=limit).sort("_id", -1)
)
) )
cursor = None cursor = None
if outbox_data and len(outbox_data) == limit: if inbox_data and len(inbox_data) == limit:
cursor = str(outbox_data[-1]["_id"]) cursor = str(inbox_data[-1]["_id"])
out = [] return render_template("stream.html", inbox_data=inbox_data, cursor=cursor)
objcache = {}
cached = list(
DB.objects_cache.find({"meta.part_of_stream": True}, limit=limit * 3).sort(
"meta.announce_published", -1
)
)
for c in cached:
objcache[c["object_id"]] = c["cached_object"]
for data in outbox_data:
if data["type"] == "Announce":
if (
data["activity"]["object"].startswith("http")
and data["activity"]["object"] in objcache
):
data["ref"] = {
"activity": {"object": objcache[data["activity"]["object"]]},
"meta": {},
}
out.append(data)
else:
print("OMG", data)
else:
out.append(data)
return render_template("stream.html", inbox_data=out, cursor=cursor)
@app.route("/inbox", methods=["GET", "POST"]) @app.route("/inbox", methods=["GET", "POST"])
@ -1138,8 +1146,8 @@ def inbox():
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.inbox, DB.activities,
q={"meta.deleted": False}, q={"meta.deleted": False, "box": Box.INBOX.value},
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: remove_context(doc["activity"]), map_func=lambda doc: remove_context(doc["activity"]),
) )
@ -1198,9 +1206,9 @@ def api_debug():
return flask_jsonify(message="DB dropped") return flask_jsonify(message="DB dropped")
return flask_jsonify( return flask_jsonify(
inbox=DB.inbox.count(), inbox=DB.activities.count({"box": Box.INBOX.value}),
outbox=DB.outbox.count(), outbox=DB.activities.count({"box": Box.OUTBOX.value}),
outbox_data=without_id(DB.outbox.find()), outbox_data=without_id(DB.activities.find({"box": Box.OUTBOX.value})),
) )
@ -1305,8 +1313,13 @@ def api_stream():
def api_block(): def api_block():
actor = _user_api_arg("actor") actor = _user_api_arg("actor")
existing = DB.outbox.find_one( existing = DB.activities.find_one(
{"type": ActivityType.BLOCK.value, "activity.object": actor, "meta.undo": False} {
"box": Box.OUTBOX.value,
"type": ActivityType.BLOCK.value,
"activity.object": actor,
"meta.undo": False,
}
) )
if existing: if existing:
return _user_api_response(activity=existing["activity"]["id"]) return _user_api_response(activity=existing["activity"]["id"])
@ -1346,14 +1359,7 @@ def followers():
followers = [ followers = [
ACTOR_SERVICE.get(doc["remote_actor"]) for doc in DB.followers.find(limit=50) ACTOR_SERVICE.get(doc["remote_actor"]) for doc in DB.followers.find(limit=50)
] ]
return render_template( return render_template("followers.html", followers_data=followers)
"followers.html",
me=ME,
notes=DB.inbox.find({"object.object.type": "Note"}).count(),
followers=DB.followers.count(),
following=DB.following.count(),
followers_data=followers,
)
@app.route("/following") @app.route("/following")
@ -1370,30 +1376,27 @@ def following():
following = [ following = [
ACTOR_SERVICE.get(doc["remote_actor"]) for doc in DB.following.find(limit=50) ACTOR_SERVICE.get(doc["remote_actor"]) for doc in DB.following.find(limit=50)
] ]
return render_template( return render_template("following.html", following_data=following)
"following.html",
me=ME,
notes=DB.inbox.find({"object.object.type": "Note"}).count(),
followers=DB.followers.count(),
following=DB.following.count(),
following_data=following,
)
@app.route("/tags/<tag>") @app.route("/tags/<tag>")
def tags(tag): def tags(tag):
if not DB.outbox.count( if not DB.activities.count(
{"activity.object.tag.type": "Hashtag", "activity.object.tag.name": "#" + tag} {
"box": Box.OUTBOX.value,
"activity.object.tag.type": "Hashtag",
"activity.object.tag.name": "#" + tag,
}
): ):
abort(404) abort(404)
if not is_api_request(): if not is_api_request():
return render_template( return render_template(
"tags.html", "tags.html",
tag=tag, tag=tag,
outbox_data=DB.outbox.find( outbox_data=DB.activities.find(
{ {
"type": "Create", "box": Box.OUTBOX.value,
"activity.object.type": "Note", "type": ActivityType.CREATE.value,
"meta.deleted": False, "meta.deleted": False,
"activity.object.tag.type": "Hashtag", "activity.object.tag.type": "Hashtag",
"activity.object.tag.name": "#" + tag, "activity.object.tag.name": "#" + tag,
@ -1401,6 +1404,7 @@ def tags(tag):
), ),
) )
q = { q = {
"box": Box.OUTBOX.value,
"meta.deleted": False, "meta.deleted": False,
"meta.undo": False, "meta.undo": False,
"type": ActivityType.CREATE.value, "type": ActivityType.CREATE.value,
@ -1409,7 +1413,7 @@ def tags(tag):
} }
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.outbox, DB.activities,
q=q, q=q,
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: doc["activity"]["object"]["id"], map_func=lambda doc: doc["activity"]["object"]["id"],
@ -1423,9 +1427,9 @@ def liked():
if not is_api_request(): if not is_api_request():
return render_template( return render_template(
"liked.html", "liked.html",
me=ME, liked=DB.activities.find(
liked=DB.outbox.find(
{ {
"box": Box.OUTBOX.value,
"type": ActivityType.LIKE.value, "type": ActivityType.LIKE.value,
"meta.deleted": False, "meta.deleted": False,
"meta.undo": False, "meta.undo": False,
@ -1436,7 +1440,7 @@ def liked():
q = {"meta.deleted": False, "meta.undo": False, "type": ActivityType.LIKE.value} q = {"meta.deleted": False, "meta.undo": False, "type": ActivityType.LIKE.value}
return jsonify( return jsonify(
**activitypub.build_ordered_collection( **activitypub.build_ordered_collection(
DB.outbox, DB.activities,
q=q, q=q,
cursor=request.args.get("cursor"), cursor=request.args.get("cursor"),
map_func=lambda doc: doc["activity"]["object"], map_func=lambda doc: doc["activity"]["object"],

View file

@ -23,16 +23,15 @@
<div id="notes"> <div id="notes">
{% for item in outbox_data %} {% for item in outbox_data %}
{% if item.type == 'Announce' %} {% if item | has_type('Announce') %}
{% set boost_actor = item.activity.actor | get_actor %} {% set boost_actor = item.activity.actor | get_actor %}
<p style="margin-left:65px;padding-bottom:5px;"><span class="bar-item"><a style="color:#808080;" href="{{ boost_actor.url }}">{{ boost_actor.name }}</a> boosted</span></p> <p style="margin-left:65px;padding-bottom:5px;"><span class="bar-item"><a style="color:#808080;" href="{{ boost_actor.url }}">{{ boost_actor.name }}</a> boosted</span></p>
{% if item.ref %} {% if item.meta.object %}
{{ utils.display_note(item.ref, ui=False) }} {{ utils.display_note(item.meta.object, ui=False) }}
{% endif %}
{% elif item.type == 'Create' %}
{{ utils.display_note(item) }}
{% endif %} {% endif %}
{% elif item | has_type('Create') %}
{{ utils.display_note(item.activity.object, meta=item.meta) }}
{% endif %}
{% endfor %} {% endfor %}
</div> </div>

18
templates/liked.html Normal file
View file

@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% import 'utils.html' as utils %}
{% block content %}
<div class="h-feed" id="container">
<data class="p-name" value="{{config.USERNAME}} liked"></data>
{% include "header.html" %}
<div id="notes">
{% for item in liked %}
{% if item.meta.object %}
{{ utils.display_note(item.meta.object) }}
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -8,26 +8,32 @@
<div id="notes"> <div id="notes">
{% for item in inbox_data %} {% for item in inbox_data %}
{% if item.type == 'Create' %} {% if item | has_type('Create') %}
{{ utils.display_note(item, ui=True) }} {{ utils.display_note(item.activity.object, ui=True) }}
{% else %} {% else %}
{% if item.type == 'Announce' %} {% if item | has_type('Announce') %}
{% set boost_actor = item.activity.actor | get_actor %} {% set boost_actor = item.activity.actor | get_actor %}
<p style="margin-left:70px;padding-bottom:5px;"><span class="bar-item"><a style="color:#808080;" href="{{ boost_actor.url }}">{{ boost_actor.name or boost_actor.preferredUsername }}</a> boosted</span></p> <p style="margin-left:70px;padding-bottom:5px;"><span class="bar-item"><a style="color:#808080;" href="{{ boost_actor.url }}">{{ boost_actor.name or boost_actor.preferredUsername }}</a> boosted</span></p>
{% if item.ref %} {% if item.meta.object %}
{{ utils.display_note(item.ref, ui=True) }} {{ utils.display_note(item.meta.object, ui=True) }}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if item.type == 'Follow' %} {% if item | has_type('Follow') %}
<p>{{ item.activity.actor }} followed you</p> <p style="margin-left:70px;padding-bottom:5px;"><span class="bar-item">new follower</span> <!-- <a href="" class="bar-item">follow back</a></p> -->
{% elif item.type == 'Accept' %} <div style="height: 100px;">
<p>you followed {{ item.activity.actor }}</p> {{ utils.display_actor_inline(item.activity.actor | get_actor, size=50) }}
{% elif item.type == 'Undo' %} </div>
<p>{{ item.activity.actor }} unfollowed you</p>
{% elif item | has_type('Accept') %}
<p style="margin-left:70px;padding-bottom:5px;"><span class="bar-item">you started following</span></p>
<div style="height: 100px;">
{{ utils.display_actor_inline(item.activity.actor | get_actor, size=50) }}
</div>
{% else %} {% else %}
{% endif %} {% endif %}

View file

@ -23,7 +23,7 @@
<h2>#{{ tag }}</h2> <h2>#{{ tag }}</h2>
<div id="notes"> <div id="notes">
{% for item in outbox_data %} {% for item in outbox_data %}
{{ utils.display_note(item) }} {{ utils.display_note(item.activity.object, meta=item.meta) }}
{% endfor %} {% endfor %}
</div> </div>

View file

@ -14,27 +14,10 @@
{%- endmacro %} {%- endmacro %}
{% macro display_actor(follower) -%}
<a class="actor-box-big" href="{{follower.url}}">
<div class="pure-g actor-box-wrapper">
<div class="pure-u-1-5">
{% if not follower.icon %}
<img class="actor-icon" src="/static/nopic.png">
{% else %}
<img class="actor-icon" src="{{ follower.icon.url }}">{% endif %}
</div>
<div class="pure-u-4-5">
<h3>{{ follower.name or follower.preferredUsername }}</h3>
<small>@{{ follower.preferredUsername }}@{{ follower.url | domain }}</small>
<div>{{ follower.summary | safe }}</div>
</div>
</div>
</a>
{%- endmacro %}
{% macro display_note(item, perma=False, ui=False, likes=[], shares=[]) -%} {% macro display_note(obj, perma=False, ui=False, likes=[], shares=[], meta={}) -%}
{% set actor = item.activity.object.attributedTo | get_actor %} {% set actor = obj.attributedTo | get_actor %}
<div class="note h-entry" id="activity-{{ item['_id'].__str__() }}"> <div class="note h-entry" id="activity-{{ obj.id | permalink_id }}">
<div class="h-card p-author"> <div class="h-card p-author">
<a class="u-url u-uid no-hover" href="{{ actor.url }}"><img class="u-photo" src="{% if not actor.icon %}/static/nopic.png{% else %}{{ actor.icon.url }}{% endif %}"> <a class="u-url u-uid no-hover" href="{{ actor.url }}"><img class="u-photo" src="{% if not actor.icon %}/static/nopic.png{% else %}{{ actor.icon.url }}{% endif %}">
@ -47,78 +30,79 @@
{% if not perma %} {% if not perma %}
<span style="float:right"> <span style="float:right">
<a rel="noopener" class="u-url u-uid note-permalink l" href="{{ item.activity.object.url }}"> <a rel="noopener" class="u-url u-uid note-permalink l" href="{{ obj.url }}">
<time class="dt-published" title="{{ item.activity.object.published }}" datetime="{{ item.activity.object.published }}">{{ item.activity.object.published | format_timeago }}</time></a> <time class="dt-published" title="{{ obj.published }}" datetime="{{ obj.published }}">{{ obj.published | format_timeago }}</time></a>
</span> </span>
{% endif %} {% endif %}
{% if item.activity.object.summary %}<p class="p-summary">{{ item.activity.object.summary }}</p>{% endif %} {% if obj.summary %}<p class="p-summary">{{ obj.summary | clean }}</p>{% endif %}
<div class="note-container{% if perma %} perma{%endif%} p-name e-content"> <div class="note-container{% if perma %} perma{%endif%} p-name e-content">
{{ item.activity.object.content | safe }} {{ obj.content | clean | safe }}
</div> </div>
{% if item.activity.object.attachment %} {% if obj.attachment %}
<div style="padding:20px 0;"> <div style="padding:20px 0;">
{% if item.activity.object.attachment | not_only_imgs %} {% if obj.attachment | not_only_imgs %}
<h3 class="l">Attachment</h3> <h3 class="l">Attachment</h3>
<ul> <ul>
{% endif %} {% endif %}
{% for a in item.activity.object.attachment %} {% for a in obj.attachment %}
{% if a.url | is_img %} {% if a.url | is_img %}
<img src="{{a.url}}" class="img-attachment"> <img src="{{a.url}}" class="img-attachment">
{% else %} {% else %}
<li><a href="{{a.url}}" class="l">{% if a.filename %}{{ a.filename }}{% else %}{{ a.url }}{% endif %}</a></li> <li><a href="{{a.url}}" class="l">{% if a.filename %}{{ a.filename }}{% else %}{{ a.url }}{% endif %}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if item.activity.object.attachment | not_only_imgs %} {% if obj.attachment | not_only_imgs %}
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="bottom-bar"> <div class="bottom-bar">
{% if perma %}<span class="perma-item">{{ item.activity.object.published | format_time }}</span> {% if perma %}<span class="perma-item">{{ obj.published | format_time }}</span>
{% else %} {% else %}
<a class ="bar-item" href="{{ item.activity.object.url }}">permalink</a> <a class ="bar-item" href="{{ obj.url }}">permalink</a>
{% if item.meta.count_reply %}<a class ="bar-item" href="{{ item.activity.object.url }}"><strong>{{ item.meta.count_reply }}</strong> replies</a>{% endif %} {% if meta.count_reply %}<a class ="bar-item" href="{{ obj.url }}"><strong>{{ meta.count_reply }}</strong> replies</a>{% endif %}
{% if item.meta.count_boost %}<a class ="bar-item" href="{{ item.activity.object.url }}"><strong>{{ item.meta.count_boost }}</strong> boosts</a>{% endif %} {% if meta.count_boost %}<a class ="bar-item" href="{{ obj.url }}"><strong>{{ meta.count_boost }}</strong> boosts</a>{% endif %}
{% if item.meta.count_like %}<a class ="bar-item" href="{{ item.activity.object.url }}"><strong>{{ item.meta.count_like }}</strong> likes</a>{% endif %} {% if meta.count_like %}<a class ="bar-item" href="{{ obj.url }}"><strong>{{ meta.count_like }}</strong> likes</a>{% endif %}
{% endif %} {% endif %}
{% if ui and session.logged_in %} {% if ui and session.logged_in %}
{% set aid = item.activity.object.id | quote_plus %} {% set aid = obj.id | quote_plus %}
<a class="bar-item" href="/new?reply={{ aid }}">reply</a> <a class="bar-item" href="/new?reply={{ aid }}">reply</a>
{% set redir = request.path + "#activity-" + item['_id'].__str__() %} {% set perma_id = obj.id | permalink_id %}
{% set redir = request.path + "#activity-" + perma_id %}
{% if item.meta.boosted %} {% if meta.boosted %}
<form action="/api/undo" class="action-form" method="POST"> <form action="/api/undo" class="action-form" method="POST">
<input type="hidden" name="redirect" value="{{ redir }}"> <input type="hidden" name="redirect" value="{{ redir }}">
<input type="hidden" name="id" value="{{ item.meta.boosted }}"> <input type="hidden" name="id" value="{{ meta.boosted }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="bar-item">unboost</button> <button type="submit" class="bar-item">unboost</button>
</form> </form>
{% else %} {% else %}
<form action="/api/boost" class="action-form" method="POST"> <form action="/api/boost" class="action-form" method="POST">
<input type="hidden" name="redirect" value="{{ redir }}"> <input type="hidden" name="redirect" value="{{ redir }}">
<input type="hidden" name="id" value="{{ item.activity.object.id }}"> <input type="hidden" name="id" value="{{ obj.id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="bar-item">boost</button> <button type="submit" class="bar-item">boost</button>
</form> </form>
{% endif %} {% endif %}
{% if item.meta.liked %} {% if meta.liked %}
<form action="/api/undo" class="action-form" method="POST"> <form action="/api/undo" class="action-form" method="POST">
<input type="hidden" name="redirect" value="{{ redir }}"> <input type="hidden" name="redirect" value="{{ redir }}">
<input type="hidden" name="id" value="{{ item.meta.liked }}"> <input type="hidden" name="id" value="{{ meta.liked }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="bar-item">unlike</button> <button type="submit" class="bar-item">unlike</button>
</form> </form>
{% else %} {% else %}
<form action="/api/like" class="action-form" method="POST"> <form action="/api/like" class="action-form" method="POST">
<input type="hidden" name="redirect" value="{{ redir }}"> <input type="hidden" name="redirect" value="{{ redir }}">
<input type="hidden" name="id" value="{{ item.activity.object.id }}"> <input type="hidden" name="id" value="{{ obj.id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="bar-item">like</button> <button type="submit" class="bar-item">like</button>
</form> </form>
@ -127,17 +111,17 @@
{% endif %} {% endif %}
{% if session.logged_in %} {% if session.logged_in %}
{% if item.activity.id | is_from_outbox %} {% if obj.id | is_from_outbox %}
<form action="/api/note/delete" class="action-form" method="POST"> <form action="/api/note/delete" class="action-form" method="POST">
<input type="hidden" name="redirect" value="{{ redir }}"> <input type="hidden" name="redirect" value="{{ redir }}">
<input type="hidden" name="id" value="{{ item.activity.object.id }}"> <input type="hidden" name="id" value="{{ obj.id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="bar-item">delete</button> <button type="submit" class="bar-item">delete</button>
</form> </form>
{% else %} {% else %}
<form action="/api/block" class="action-form" method="POST"> <form action="/api/block" class="action-form" method="POST">
<input type="hidden" name="redirect" value="{{ redir }}"> <input type="hidden" name="redirect" value="{{ redir }}">
<input type="hidden" name="actor" value="{{ item.activity.actor }}"> <input type="hidden" name="actor" value="{{ actor.id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="bar-item">block</button> <button type="submit" class="bar-item">block</button>
</form> </form>
@ -151,14 +135,14 @@
<div style="padding-top:20px;" class="pure-g"> <div style="padding-top:20px;" class="pure-g">
{% if likes %} {% if likes %}
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<h4 style="font-weight:normal"><strong>{{ item.meta.count_like }}</strong> likes</h4>{% for like in likes %} <h4 style="font-weight:normal"><strong>{{ meta.count_like }}</strong> likes</h4>{% for like in likes %}
{{ display_actor_inline(like) }} {{ display_actor_inline(like) }}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if shares %} {% if shares %}
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<h4 style="font-weight:normal"><strong>{{ item.meta.count_boost }}</strong> boosts</h4>{% for boost in shares %} <h4 style="font-weight:normal"><strong>{{ meta.count_boost }}</strong> boosts</h4>{% for boost in shares %}
{{ display_actor_inline(boost) }} {{ display_actor_inline(boost) }}
{% endfor %} {% endfor %}
</div> </div>
@ -177,9 +161,9 @@
{% macro display_thread(thread, likes=[], shares=[]) -%} {% macro display_thread(thread, likes=[], shares=[]) -%}
{% for reply in thread %} {% for reply in thread %}
{% if reply._requested %} {% if reply._requested %}
{{ display_note(reply, perma=True, ui=False, likes=likes, shares=shares) }} {{ display_note(reply.activity.object, perma=True, ui=False, likes=likes, shares=shares, meta=reply.meta) }}
{% else %} {% else %}
{{ display_note(reply, perma=False, ui=True) }} {{ display_note(reply.activity.object, perma=False, ui=True, meta=reply.meta) }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endmacro -%} {% endmacro -%}