Start support for manually approving followers

This commit is contained in:
Thomas Sileo 2022-08-02 20:14:40 +02:00
parent 9f3956db67
commit a1a9ec3f7c
10 changed files with 272 additions and 10 deletions

View file

@ -95,7 +95,7 @@ ME = {
+ "/inbox",
},
"url": config.ID,
"manuallyApprovesFollowers": False,
"manuallyApprovesFollowers": config.CONFIG.manually_approves_followers,
"attachment": [],
"icon": {
"mediaType": mimetypes.guess_type(config.CONFIG.icon_url)[0],

View file

@ -218,6 +218,7 @@ async def get_actors_metadata(
select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where(
models.OutboxObject.ap_type == "Follow",
models.OutboxObject.undone_by_outbox_object_id.is_(None),
models.OutboxObject.activity_object_ap_id.in_(ap_actor_ids),
)
)
}

View file

@ -616,6 +616,30 @@ async def admin_actions_delete(
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/accept_incoming_follow")
async def admin_actions_accept_incoming_follow(
request: Request,
notification_id: int = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
await boxes.send_accept(db_session, notification_id)
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/reject_incoming_follow")
async def admin_actions_reject_incoming_follow(
request: Request,
notification_id: int = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
await boxes.send_reject(db_session, notification_id)
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/like")
async def admin_actions_like(
request: Request,

View file

@ -27,6 +27,7 @@ from app.actor import save_actor
from app.ap_object import RemoteObject
from app.config import BASE_URL
from app.config import ID
from app.config import MANUALLY_APPROVES_FOLLOWERS
from app.database import AsyncSession
from app.outgoing_activities import new_outgoing_activity
from app.source import markdownify
@ -654,6 +655,22 @@ async def _get_followers_recipients(
}
async def get_notification_by_id(
db_session: AsyncSession, notification_id: int
) -> models.Notification | None:
return (
await db_session.execute(
select(models.Notification)
.where(models.Notification.id == notification_id)
.options(
joinedload(models.Notification.inbox_object).options(
joinedload(models.InboxObject.actor)
),
)
)
).scalar_one_or_none() # type: ignore
async def get_inbox_object_by_ap_id(
db_session: AsyncSession, ap_id: str
) -> models.InboxObject | None:
@ -832,6 +849,57 @@ async def _handle_follow_follow_activity(
from_actor: models.Actor,
inbox_object: models.InboxObject,
) -> None:
if MANUALLY_APPROVES_FOLLOWERS:
notif = models.Notification(
notification_type=models.NotificationType.PENDING_INCOMING_FOLLOWER,
actor_id=from_actor.id,
inbox_object_id=inbox_object.id,
)
db_session.add(notif)
return None
await _send_accept(db_session, from_actor, inbox_object)
async def _get_incoming_follow_from_notification_id(
db_session: AsyncSession,
notification_id: int,
) -> tuple[models.Notification, models.InboxObject]:
notif = await get_notification_by_id(db_session, notification_id)
if notif is None:
raise ValueError(f"Notification {notification_id=} not found")
if notif.inbox_object is None:
raise ValueError("Should never happen")
if ap_type := notif.inbox_object.ap_type != "Follow":
raise ValueError(f"Unexpected {ap_type=}")
return notif, notif.inbox_object
async def send_accept(
db_session: AsyncSession,
notification_id: int,
) -> None:
notif, incoming_follow_request = await _get_incoming_follow_from_notification_id(
db_session, notification_id
)
await _send_accept(
db_session, incoming_follow_request.actor, incoming_follow_request
)
notif.is_accepted = True
await db_session.commit()
async def _send_accept(
db_session: AsyncSession,
from_actor: models.Actor,
inbox_object: models.InboxObject,
) -> None:
follower = models.Follower(
actor_id=from_actor.id,
inbox_object_id=inbox_object.id,
@ -852,7 +920,9 @@ async def _handle_follow_follow_activity(
"actor": ID,
"object": inbox_object.ap_id,
}
outbox_activity = await save_outbox_object(db_session, reply_id, reply)
outbox_activity = await save_outbox_object(
db_session, reply_id, reply, relates_to_inbox_object_id=inbox_object.id
)
if not outbox_activity.id:
raise ValueError("Should never happen")
await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id)
@ -864,6 +934,49 @@ async def _handle_follow_follow_activity(
db_session.add(notif)
async def send_reject(
db_session: AsyncSession,
notification_id: int,
) -> None:
notif, incoming_follow_request = await _get_incoming_follow_from_notification_id(
db_session, notification_id
)
await _send_reject(
db_session, incoming_follow_request.actor, incoming_follow_request
)
notif.is_rejected = True
await db_session.commit()
async def _send_reject(
db_session: AsyncSession,
from_actor: models.Actor,
inbox_object: models.InboxObject,
) -> None:
# Reply with an Accept
reply_id = allocate_outbox_id()
reply = {
"@context": ap.AS_CTX,
"id": outbox_object_id(reply_id),
"type": "Reject",
"actor": ID,
"object": inbox_object.ap_id,
}
outbox_activity = await save_outbox_object(
db_session, reply_id, reply, relates_to_inbox_object_id=inbox_object.id
)
if not outbox_activity.id:
raise ValueError("Should never happen")
await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id)
notif = models.Notification(
notification_type=models.NotificationType.REJECTED_FOLLOWER,
actor_id=from_actor.id,
)
db_session.add(notif)
async def _handle_undo_activity(
db_session: AsyncSession,
from_actor: models.Actor,

View file

@ -42,6 +42,7 @@ class Config(pydantic.BaseModel):
secret: str
debug: bool = False
trusted_hosts: list[str] = ["127.0.0.1"]
manually_approves_followers: bool = False
# Config items to make tests easier
sqlalchemy_database: str | None = None
@ -82,6 +83,7 @@ DOMAIN = CONFIG.domain
_SCHEME = "https" if CONFIG.https else "http"
ID = f"{_SCHEME}://{DOMAIN}"
USERNAME = CONFIG.username
MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers
BASE_URL = ID
DEBUG = CONFIG.debug
DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db"

View file

@ -523,6 +523,8 @@ class PollAnswer(Base):
@enum.unique
class NotificationType(str, enum.Enum):
NEW_FOLLOWER = "new_follower"
PENDING_INCOMING_FOLLOWER = "pending_incoming_follower"
REJECTED_FOLLOWER = "rejected_follower"
UNFOLLOW = "unfollow"
FOLLOW_REQUEST_ACCEPTED = "follow_request_accepted"
@ -563,6 +565,9 @@ class Notification(Base):
)
webmention = relationship(Webmention, uselist=False)
is_accepted = Column(Boolean, nullable=True)
is_rejected = Column(Boolean, nullable=True)
outbox_fts = Table(
"outbox_fts",

View file

@ -22,6 +22,10 @@
{%- if notif.notification_type.value == "new_follower" %}
{{ notif_actor_action(notif, "followed you") }}
{{ utils.display_actor(notif.actor, actors_metadata) }}
{%- elif notif.notification_type.value == "pending_incoming_follower" %}
{{ notif_actor_action(notif, "sent a follow request") }}
{{ utils.display_actor(notif.actor, actors_metadata, pending_incoming_follow_notif=notif) }}
{% elif notif.notification_type.value == "rejected_follower" %}
{% elif notif.notification_type.value == "unfollow" %}
{{ notif_actor_action(notif, "unfollowed you") }}
{{ utils.display_actor(notif.actor, actors_metadata) }}

View file

@ -33,6 +33,24 @@
</form>
{% endmacro %}
{% macro admin_accept_incoming_follow_button(notif) %}
<form action="{{ request.url_for("admin_actions_accept_incoming_follow") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url() }}
<input type="hidden" name="notification_id" value="{{ notif.id }}">
<input type="submit" value="accept follow">
</form>
{% endmacro %}
{% macro admin_reject_incoming_follow_button(notif) %}
<form action="{{ request.url_for("admin_actions_reject_incoming_follow") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url() }}
<input type="hidden" name="notification_id" value="{{ notif.id }}">
<input type="submit" value="reject follow">
</form>
{% endmacro %}
{% macro admin_like_button(ap_object_id, permalink_id) %}
<form action="{{ request.url_for("admin_actions_like") }}" method="POST">
{{ embed_csrf_token() }}
@ -197,7 +215,7 @@
{% endmacro %}
{% macro display_actor(actor, actors_metadata={}, embedded=False, with_details=False) %}
{% macro display_actor(actor, actors_metadata={}, embedded=False, with_details=False, pending_incoming_follow_notif=None) %}
{% set metadata = actors_metadata.get(actor.ap_id) %}
{% if not embedded %}
@ -243,6 +261,20 @@
<li>{{ admin_block_button(actor) }}</li>
{% endif %}
{% endif %}
{% if pending_incoming_follow_notif %}
{% if not pending_incoming_follow_notif.is_accepted and not pending_incoming_follow_notif.is_rejected %}
<li>
{{ admin_accept_incoming_follow_button(pending_incoming_follow_notif) }}
</li>
<li>
{{ admin_reject_incoming_follow_button(pending_incoming_follow_notif) }}
</li>
{% elif pending_incoming_follow_notif.is_accepted %}
<li>accepted</li>
{% else %}
<li>rejected</li>
{% endif %}
{% endif %}
</ul>
</nav>
</div>