Start support for manually approving followers
This commit is contained in:
parent
9f3956db67
commit
a1a9ec3f7c
10 changed files with 272 additions and 10 deletions
|
@ -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],
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
24
app/admin.py
24
app/admin.py
|
@ -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,
|
||||
|
|
115
app/boxes.py
115
app/boxes.py
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue