mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-11-15 03:04:28 +00:00
Improve follower/following management
This commit is contained in:
parent
fa1cc4cb4c
commit
a49ba89a7e
6 changed files with 157 additions and 19 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
.git/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
data/
|
data/
|
||||||
|
data2/
|
||||||
tests/
|
tests/
|
||||||
|
|
|
@ -33,6 +33,7 @@ from core.db import find_one_activity
|
||||||
from core.db import update_many_activities
|
from core.db import update_many_activities
|
||||||
from core.db import update_one_activity
|
from core.db import update_one_activity
|
||||||
from core.meta import Box
|
from core.meta import Box
|
||||||
|
from core.meta import FollowStatus
|
||||||
from core.meta import MetaKey
|
from core.meta import MetaKey
|
||||||
from core.meta import by_object_id
|
from core.meta import by_object_id
|
||||||
from core.meta import by_remote_id
|
from core.meta import by_remote_id
|
||||||
|
@ -52,6 +53,16 @@ SIG_AUTH = HTTPSigAuth(KEY)
|
||||||
|
|
||||||
MY_PERSON = ap.Person(**ME)
|
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:
|
def _remove_id(doc: ap.ObjectType) -> ap.ObjectType:
|
||||||
"""Helper for removing MongoDB's `_id` field."""
|
"""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
|
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(
|
DB.activities.insert_one(
|
||||||
{
|
{
|
||||||
"box": box.value,
|
"box": box.value,
|
||||||
|
@ -135,6 +151,7 @@ def save(box: Box, activity: ap.BaseActivity) -> None:
|
||||||
MetaKey.PUBLISHED.value: activity.published
|
MetaKey.PUBLISHED.value: activity.published
|
||||||
if activity.published
|
if activity.published
|
||||||
else now(),
|
else now(),
|
||||||
|
**extra,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,10 +9,12 @@ from little_boxes.errors import NotAnActivityError
|
||||||
import config
|
import config
|
||||||
from core.activitypub import _answer_key
|
from core.activitypub import _answer_key
|
||||||
from core.activitypub import handle_replies
|
from core.activitypub import handle_replies
|
||||||
|
from core.activitypub import is_from_outbox
|
||||||
from core.activitypub import post_to_outbox
|
from core.activitypub import post_to_outbox
|
||||||
from core.activitypub import update_cached_actor
|
from core.activitypub import update_cached_actor
|
||||||
from core.db import DB
|
from core.db import DB
|
||||||
from core.db import update_one_activity
|
from core.db import update_one_activity
|
||||||
|
from core.meta import FollowStatus
|
||||||
from core.meta import MetaKey
|
from core.meta import MetaKey
|
||||||
from core.meta import by_object_id
|
from core.meta import by_object_id
|
||||||
from core.meta import by_remote_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)
|
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
|
@process_inbox.register
|
||||||
def _undo_process_inbox(activity: ap.Undo, new_meta: _NewMeta) -> None:
|
def _undo_process_inbox(activity: ap.Undo, new_meta: _NewMeta) -> None:
|
||||||
_logger.info(f"process_inbox activity={activity!r}")
|
_logger.info(f"process_inbox activity={activity!r}")
|
||||||
|
|
18
core/meta.py
18
core/meta.py
|
@ -18,6 +18,13 @@ class Box(Enum):
|
||||||
REPLIES = "replies"
|
REPLIES = "replies"
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class FollowStatus(Enum):
|
||||||
|
WAITING = "waiting"
|
||||||
|
ACCEPTED = "accepted"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class MetaKey(Enum):
|
class MetaKey(Enum):
|
||||||
NOTIFICATION = "notification"
|
NOTIFICATION = "notification"
|
||||||
|
@ -38,6 +45,9 @@ class MetaKey(Enum):
|
||||||
OBJECT_ACTOR_ID = "object_actor_id"
|
OBJECT_ACTOR_ID = "object_actor_id"
|
||||||
OBJECT_ACTOR_HASH = "object_actor_hash"
|
OBJECT_ACTOR_HASH = "object_actor_hash"
|
||||||
PUBLIC = "public"
|
PUBLIC = "public"
|
||||||
|
|
||||||
|
FOLLOW_STATUS = "follow_status"
|
||||||
|
|
||||||
THREAD_ROOT_PARENT = "thread_root_parent"
|
THREAD_ROOT_PARENT = "thread_root_parent"
|
||||||
|
|
||||||
IN_REPLY_TO_SELF = "in_reply_to_self"
|
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}
|
return {"type": type_.value}
|
||||||
|
|
||||||
|
|
||||||
|
def follow_request_accepted() -> _SubQuery:
|
||||||
|
return flag(MetaKey.FOLLOW_STATUS, FollowStatus.ACCEPTED.value)
|
||||||
|
|
||||||
|
|
||||||
def not_undo() -> _SubQuery:
|
def not_undo() -> _SubQuery:
|
||||||
return flag(MetaKey.UNDO, False)
|
return flag(MetaKey.UNDO, False)
|
||||||
|
|
||||||
|
@ -95,6 +109,10 @@ def by_actor(actor: ap.BaseActivity) -> _SubQuery:
|
||||||
return flag(MetaKey.ACTOR_ID, actor.id)
|
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:
|
def by_object_id(object_id: str) -> _SubQuery:
|
||||||
return flag(MetaKey.OBJECT_ID, object_id)
|
return flag(MetaKey.OBJECT_ID, object_id)
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,16 @@ from core import activitypub
|
||||||
from core.db import DB
|
from core.db import DB
|
||||||
from core.db import find_activities
|
from core.db import find_activities
|
||||||
from core.db import update_one_activity
|
from core.db import update_one_activity
|
||||||
|
from core.meta import FollowStatus
|
||||||
from core.meta import MetaKey
|
from core.meta import MetaKey
|
||||||
from core.meta import _meta
|
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 Migration
|
||||||
from utils.migrations import logger
|
from utils.migrations import logger
|
||||||
from utils.migrations import perform # noqa: just here for export
|
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}}
|
{"_id": data["_id"]}, {"$set": {"meta.actor": actor}}
|
||||||
)
|
)
|
||||||
except Exception:
|
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."""
|
"""Add the `meta.published` field to old activities."""
|
||||||
|
|
||||||
def migrate(self) -> None:
|
def migrate(self) -> None:
|
||||||
|
@ -180,4 +188,68 @@ class _20190808_MetaPublishedMigration(Migration):
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
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"}},
|
||||||
|
)
|
||||||
|
|
|
@ -5,12 +5,12 @@ from datetime import timezone
|
||||||
from functools import singledispatch
|
from functools import singledispatch
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from little_boxes import activitypub as ap
|
from little_boxes import activitypub as ap
|
||||||
|
|
||||||
from config import BASE_URL
|
|
||||||
from config import DB
|
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.db import find_one_activity
|
||||||
from core.meta import MetaKey
|
from core.meta import MetaKey
|
||||||
from core.meta import _meta
|
from core.meta import _meta
|
||||||
|
@ -27,16 +27,6 @@ _logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_NewMeta = Dict[str, Any]
|
_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:
|
def _flag_as_notification(activity: ap.BaseActivity, new_meta: _NewMeta) -> None:
|
||||||
new_meta.update(
|
new_meta.update(
|
||||||
|
@ -83,6 +73,16 @@ def _accept_set_inbox_flags(activity: ap.Accept, new_meta: _NewMeta) -> None:
|
||||||
return 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
|
@set_inbox_flags.register
|
||||||
def _follow_set_inbox_flags(activity: ap.Follow, new_meta: _NewMeta) -> None:
|
def _follow_set_inbox_flags(activity: ap.Follow, new_meta: _NewMeta) -> None:
|
||||||
"""Handle notification for new followers."""
|
"""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:
|
def _like_set_inbox_flags(activity: ap.Like, new_meta: _NewMeta) -> None:
|
||||||
_logger.info(f"set_inbox_flags activity={activity!r}")
|
_logger.info(f"set_inbox_flags activity={activity!r}")
|
||||||
# Is it a Like of local acitivty/from the outbox
|
# 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 it as a notification
|
||||||
_flag_as_notification(activity, new_meta)
|
_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}")
|
_logger.info(f"set_inbox_flags activity={activity!r}")
|
||||||
obj = activity.get_object()
|
obj = activity.get_object()
|
||||||
# Is it a Annnounce/boost of local acitivty/from the outbox
|
# 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 it as a notification
|
||||||
_flag_as_notification(activity, new_meta)
|
_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()
|
in_reply_to = obj.get_in_reply_to()
|
||||||
|
|
||||||
# Check if it's a local reply
|
# 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
|
# TODO(tsileo): fetch the reply to check for poll answers more precisely
|
||||||
# reply_of = ap.fetch_remote_activity(in_reply_to)
|
# 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
|
# Check for mention
|
||||||
for mention in obj.get_mentions():
|
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 it as a notification
|
||||||
_flag_as_notification(activity, new_meta)
|
_flag_as_notification(activity, new_meta)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue