Improve follower/following management

This commit is contained in:
Thomas Sileo 2019-09-01 10:26:25 +02:00
parent fa1cc4cb4c
commit a49ba89a7e
6 changed files with 157 additions and 19 deletions

View file

@ -1,3 +1,5 @@
.git/
__pycache__/
data/
data2/
tests/

View file

@ -33,6 +33,7 @@ from core.db import find_one_activity
from core.db import update_many_activities
from core.db import update_one_activity
from core.meta import Box
from core.meta import FollowStatus
from core.meta import MetaKey
from core.meta import by_object_id
from core.meta import by_remote_id
@ -52,6 +53,16 @@ SIG_AUTH = HTTPSigAuth(KEY)
MY_PERSON = ap.Person(**ME)
_LOCAL_NETLOC = urlparse(BASE_URL).netloc
def is_from_outbox(activity: ap.BaseActivity) -> bool:
return activity.id.startswith(BASE_URL)
def is_local_url(url: str) -> bool:
return urlparse(url).netloc == _LOCAL_NETLOC
def _remove_id(doc: ap.ObjectType) -> ap.ObjectType:
"""Helper for removing MongoDB's `_id` field."""
@ -116,6 +127,11 @@ def save(box: Box, activity: ap.BaseActivity) -> None:
actor_id = activity.get_actor().id
# Set some "type"-related neta
extra = {}
if box == Box.OUTBOX and activity.has_type(ap.Follow):
extra[MetaKey.FOLLOW_STATUS.value] = FollowStatus.WAITING.value
DB.activities.insert_one(
{
"box": box.value,
@ -135,6 +151,7 @@ def save(box: Box, activity: ap.BaseActivity) -> None:
MetaKey.PUBLISHED.value: activity.published
if activity.published
else now(),
**extra,
},
}
)

View file

@ -9,10 +9,12 @@ from little_boxes.errors import NotAnActivityError
import config
from core.activitypub import _answer_key
from core.activitypub import handle_replies
from core.activitypub import is_from_outbox
from core.activitypub import post_to_outbox
from core.activitypub import update_cached_actor
from core.db import DB
from core.db import update_one_activity
from core.meta import FollowStatus
from core.meta import MetaKey
from core.meta import by_object_id
from core.meta import by_remote_id
@ -174,6 +176,33 @@ def _follow_process_inbox(activity: ap.Follow, new_meta: _NewMeta) -> None:
post_to_outbox(accept)
def _update_follow_status(follow: ap.BaseActivity, status: FollowStatus) -> None:
if not follow.has_type(ap.Follow) or not is_from_outbox(follow):
_logger.warning(
"received an Accept/Reject from an unexpected activity: {follow!r}"
)
return None
update_one_activity(
by_remote_id(follow.id), upsert({MetaKey.FOLLOW_STATUS: status.value})
)
@process_inbox.register
def _accept_process_inbox(activity: ap.Accept, new_meta: _NewMeta) -> None:
_logger.info(f"process_inbox activity={activity!r}")
# Set a flag on the follow
follow = activity.get_object()
_update_follow_status(follow, FollowStatus.ACCEPTED)
@process_inbox.register
def _reject_process_inbox(activity: ap.Reject, new_meta: _NewMeta) -> None:
_logger.info(f"process_inbox activity={activity!r}")
follow = activity.get_object()
_update_follow_status(follow, FollowStatus.REJECTED)
@process_inbox.register
def _undo_process_inbox(activity: ap.Undo, new_meta: _NewMeta) -> None:
_logger.info(f"process_inbox activity={activity!r}")

View file

@ -18,6 +18,13 @@ class Box(Enum):
REPLIES = "replies"
@unique
class FollowStatus(Enum):
WAITING = "waiting"
ACCEPTED = "accepted"
REJECTED = "rejected"
@unique
class MetaKey(Enum):
NOTIFICATION = "notification"
@ -38,6 +45,9 @@ class MetaKey(Enum):
OBJECT_ACTOR_ID = "object_actor_id"
OBJECT_ACTOR_HASH = "object_actor_hash"
PUBLIC = "public"
FOLLOW_STATUS = "follow_status"
THREAD_ROOT_PARENT = "thread_root_parent"
IN_REPLY_TO_SELF = "in_reply_to_self"
@ -83,6 +93,10 @@ def by_type(type_: Union[ap.ActivityType, List[ap.ActivityType]]) -> _SubQuery:
return {"type": type_.value}
def follow_request_accepted() -> _SubQuery:
return flag(MetaKey.FOLLOW_STATUS, FollowStatus.ACCEPTED.value)
def not_undo() -> _SubQuery:
return flag(MetaKey.UNDO, False)
@ -95,6 +109,10 @@ def by_actor(actor: ap.BaseActivity) -> _SubQuery:
return flag(MetaKey.ACTOR_ID, actor.id)
def by_actor_id(actor_id: str) -> _SubQuery:
return flag(MetaKey.ACTOR_ID, actor_id)
def by_object_id(object_id: str) -> _SubQuery:
return flag(MetaKey.OBJECT_ID, object_id)

View file

@ -10,8 +10,16 @@ from core import activitypub
from core.db import DB
from core.db import find_activities
from core.db import update_one_activity
from core.meta import FollowStatus
from core.meta import MetaKey
from core.meta import _meta
from core.meta import by_actor_id
from core.meta import by_actor
from core.meta import by_remote_id
from core.meta import by_type
from core.meta import in_inbox
from core.meta import not_undo
from core.meta import upsert
from utils.migrations import Migration
from utils.migrations import logger
from utils.migrations import perform # noqa: just here for export
@ -156,10 +164,10 @@ class _2_FollowMigration(Migration):
{"_id": data["_id"]}, {"$set": {"meta.actor": actor}}
)
except Exception:
logger.exception("failed to process actor {data!r}")
logger.exception(f"failed to process actor {data!r}")
class _20190808_MetaPublishedMigration(Migration):
class _20190830_MetaPublishedMigration(Migration):
"""Add the `meta.published` field to old activities."""
def migrate(self) -> None:
@ -180,4 +188,68 @@ class _20190808_MetaPublishedMigration(Migration):
)
except Exception:
logger.exception("failed to process activity {data!r}")
logger.exception(f"failed to process activity {data!r}")
class _20190830_FollowFollowBackMigration(Migration):
"""Add the new meta flags for tracking accepted/rejected status and following/follows back info."""
def migrate(self) -> None:
for data in find_activities({**by_type(ap.ActivityType.ACCEPT), **in_inbox()}):
try:
update_one_activity(
{**by_type(ap.ActivityType.FOLLOW), **by_remote_id(data["meta"]["object_id"])},
upsert({MetaKey.FOLLOW_STATUS: FollowStatus.ACCEPTED.value}),
)
# Check if we are following this actor
follow_query = {
**in_inbox(),
**by_type(ap.ActivityType.FOLLOW),
**by_actor_id(data["meta"]["actor_id"]),
**not_undo(),
}
raw_follow = DB.activities.find_one(follow_query)
if raw_follow:
DB.activities.update_many(
follow_query,
{"$set": {_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): True}},
)
except Exception:
logger.exception(f"failed to process activity {data!r}")
for data in find_activities({**by_type(ap.ActivityType.REJECT), **in_inbox()}):
try:
update_one_activity(
{**by_type(ap.ActivityType.FOLLOW), **by_remote_id(data["meta"]["object_id"])},
upsert({MetaKey.FOLLOW_STATUS: FollowStatus.REJECTED.value}),
)
except Exception:
logger.exception(f"failed to process activity {data!r}")
for data in find_activities({**by_type(ap.ActivityType.FOLLOW), **in_inbox()}):
try:
accept_query = {
**in_inbox(),
**by_type(ap.ActivityType.ACCEPT),
**by_actor_id(data["meta"]["actor_id"]),
**not_undo(),
}
raw_accept = DB.activities.find_one(accept_query)
if raw_accept:
DB.activities.update_many(
accept_query,
{"$set": {_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): True}},
)
except Exception:
logger.exception(f"failed to process activity {data!r}")
DB.activities.update_many(
{
**by_type(ap.ActivityType.FOLLOW),
**in_inbox(),
"meta.follow_status": {"$exists": False},
},
{"$set": {"meta.follow_status": "waiting"}},
)

View file

@ -5,12 +5,12 @@ from datetime import timezone
from functools import singledispatch
from typing import Any
from typing import Dict
from urllib.parse import urlparse
from little_boxes import activitypub as ap
from config import BASE_URL
from config import DB
from core.activitypub import is_from_outbox
from core.activitypub import is_local_url
from core.db import find_one_activity
from core.meta import MetaKey
from core.meta import _meta
@ -27,16 +27,6 @@ _logger = logging.getLogger(__name__)
_NewMeta = Dict[str, Any]
_LOCAL_NETLOC = urlparse(BASE_URL).netloc
def _is_from_outbox(activity: ap.BaseActivity) -> bool:
return activity.id.startswith(BASE_URL)
def _is_local(url: str) -> bool:
return urlparse(url).netloc == _LOCAL_NETLOC
def _flag_as_notification(activity: ap.BaseActivity, new_meta: _NewMeta) -> None:
new_meta.update(
@ -83,6 +73,16 @@ def _accept_set_inbox_flags(activity: ap.Accept, new_meta: _NewMeta) -> None:
return None
@set_inbox_flags.register
def _reject_set_inbox_flags(activity: ap.Reject, new_meta: _NewMeta) -> None:
"""Handle notifications for "rejected" following requests."""
_logger.info(f"set_inbox_flags activity={activity!r}")
# This Accept will be a "You started following $actor" notification
_flag_as_notification(activity, new_meta)
_set_flag(new_meta, MetaKey.GC_KEEP)
return None
@set_inbox_flags.register
def _follow_set_inbox_flags(activity: ap.Follow, new_meta: _NewMeta) -> None:
"""Handle notification for new followers."""
@ -114,7 +114,7 @@ def _follow_set_inbox_flags(activity: ap.Follow, new_meta: _NewMeta) -> None:
def _like_set_inbox_flags(activity: ap.Like, new_meta: _NewMeta) -> None:
_logger.info(f"set_inbox_flags activity={activity!r}")
# Is it a Like of local acitivty/from the outbox
if _is_from_outbox(activity.get_object()):
if is_from_outbox(activity.get_object()):
# Flag it as a notification
_flag_as_notification(activity, new_meta)
@ -132,7 +132,7 @@ def _announce_set_inbox_flags(activity: ap.Announce, new_meta: _NewMeta) -> None
_logger.info(f"set_inbox_flags activity={activity!r}")
obj = activity.get_object()
# Is it a Annnounce/boost of local acitivty/from the outbox
if _is_from_outbox(obj):
if is_from_outbox(obj):
# Flag it as a notification
_flag_as_notification(activity, new_meta)
@ -180,7 +180,7 @@ def _create_set_inbox_flags(activity: ap.Create, new_meta: _NewMeta) -> None:
in_reply_to = obj.get_in_reply_to()
# Check if it's a local reply
if in_reply_to and _is_local(in_reply_to):
if in_reply_to and is_local_url(in_reply_to):
# TODO(tsileo): fetch the reply to check for poll answers more precisely
# reply_of = ap.fetch_remote_activity(in_reply_to)
@ -199,7 +199,7 @@ def _create_set_inbox_flags(activity: ap.Create, new_meta: _NewMeta) -> None:
# Check for mention
for mention in obj.get_mentions():
if mention.href and _is_local(mention.href):
if mention.href and is_local_url(mention.href):
# Flag it as a notification
_flag_as_notification(activity, new_meta)