mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Start support for manually approving followers
This commit is contained in:
		| @@ -0,0 +1,34 @@ | |||||||
|  | """Tweak notification model | ||||||
|  |  | ||||||
|  | Revision ID: 1702e88016db | ||||||
|  | Revises: 50d26a370a65 | ||||||
|  | Create Date: 2022-08-02 15:19:57.221421+00:00 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | import sqlalchemy as sa | ||||||
|  |  | ||||||
|  | from alembic import op | ||||||
|  |  | ||||||
|  | # revision identifiers, used by Alembic. | ||||||
|  | revision = '1702e88016db' | ||||||
|  | down_revision = '50d26a370a65' | ||||||
|  | branch_labels = None | ||||||
|  | depends_on = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def upgrade() -> None: | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     with op.batch_alter_table('notifications', schema=None) as batch_op: | ||||||
|  |         batch_op.add_column(sa.Column('is_accepted', sa.Boolean(), nullable=True)) | ||||||
|  |         batch_op.add_column(sa.Column('is_rejected', sa.Boolean(), nullable=True)) | ||||||
|  |  | ||||||
|  |     # ### end Alembic commands ### | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def downgrade() -> None: | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     with op.batch_alter_table('notifications', schema=None) as batch_op: | ||||||
|  |         batch_op.drop_column('is_rejected') | ||||||
|  |         batch_op.drop_column('is_accepted') | ||||||
|  |  | ||||||
|  |     # ### end Alembic commands ### | ||||||
| @@ -95,7 +95,7 @@ ME = { | |||||||
|         + "/inbox", |         + "/inbox", | ||||||
|     }, |     }, | ||||||
|     "url": config.ID, |     "url": config.ID, | ||||||
|     "manuallyApprovesFollowers": False, |     "manuallyApprovesFollowers": config.CONFIG.manually_approves_followers, | ||||||
|     "attachment": [], |     "attachment": [], | ||||||
|     "icon": { |     "icon": { | ||||||
|         "mediaType": mimetypes.guess_type(config.CONFIG.icon_url)[0], |         "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( |             select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where( | ||||||
|                 models.OutboxObject.ap_type == "Follow", |                 models.OutboxObject.ap_type == "Follow", | ||||||
|                 models.OutboxObject.undone_by_outbox_object_id.is_(None), |                 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) |     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") | @router.post("/actions/like") | ||||||
| async def admin_actions_like( | async def admin_actions_like( | ||||||
|     request: Request, |     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.ap_object import RemoteObject | ||||||
| from app.config import BASE_URL | from app.config import BASE_URL | ||||||
| from app.config import ID | from app.config import ID | ||||||
|  | from app.config import MANUALLY_APPROVES_FOLLOWERS | ||||||
| from app.database import AsyncSession | from app.database import AsyncSession | ||||||
| from app.outgoing_activities import new_outgoing_activity | from app.outgoing_activities import new_outgoing_activity | ||||||
| from app.source import markdownify | 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( | async def get_inbox_object_by_ap_id( | ||||||
|     db_session: AsyncSession, ap_id: str |     db_session: AsyncSession, ap_id: str | ||||||
| ) -> models.InboxObject | None: | ) -> models.InboxObject | None: | ||||||
| @@ -832,6 +849,57 @@ async def _handle_follow_follow_activity( | |||||||
|     from_actor: models.Actor, |     from_actor: models.Actor, | ||||||
|     inbox_object: models.InboxObject, |     inbox_object: models.InboxObject, | ||||||
| ) -> None: | ) -> 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( |     follower = models.Follower( | ||||||
|         actor_id=from_actor.id, |         actor_id=from_actor.id, | ||||||
|         inbox_object_id=inbox_object.id, |         inbox_object_id=inbox_object.id, | ||||||
| @@ -852,7 +920,9 @@ async def _handle_follow_follow_activity( | |||||||
|         "actor": ID, |         "actor": ID, | ||||||
|         "object": inbox_object.ap_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: |     if not outbox_activity.id: | ||||||
|         raise ValueError("Should never happen") |         raise ValueError("Should never happen") | ||||||
|     await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id) |     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) |     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( | async def _handle_undo_activity( | ||||||
|     db_session: AsyncSession, |     db_session: AsyncSession, | ||||||
|     from_actor: models.Actor, |     from_actor: models.Actor, | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ class Config(pydantic.BaseModel): | |||||||
|     secret: str |     secret: str | ||||||
|     debug: bool = False |     debug: bool = False | ||||||
|     trusted_hosts: list[str] = ["127.0.0.1"] |     trusted_hosts: list[str] = ["127.0.0.1"] | ||||||
|  |     manually_approves_followers: bool = False | ||||||
|  |  | ||||||
|     # Config items to make tests easier |     # Config items to make tests easier | ||||||
|     sqlalchemy_database: str | None = None |     sqlalchemy_database: str | None = None | ||||||
| @@ -82,6 +83,7 @@ DOMAIN = CONFIG.domain | |||||||
| _SCHEME = "https" if CONFIG.https else "http" | _SCHEME = "https" if CONFIG.https else "http" | ||||||
| ID = f"{_SCHEME}://{DOMAIN}" | ID = f"{_SCHEME}://{DOMAIN}" | ||||||
| USERNAME = CONFIG.username | USERNAME = CONFIG.username | ||||||
|  | MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers | ||||||
| BASE_URL = ID | BASE_URL = ID | ||||||
| DEBUG = CONFIG.debug | DEBUG = CONFIG.debug | ||||||
| DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db" | DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db" | ||||||
|   | |||||||
| @@ -523,6 +523,8 @@ class PollAnswer(Base): | |||||||
| @enum.unique | @enum.unique | ||||||
| class NotificationType(str, enum.Enum): | class NotificationType(str, enum.Enum): | ||||||
|     NEW_FOLLOWER = "new_follower" |     NEW_FOLLOWER = "new_follower" | ||||||
|  |     PENDING_INCOMING_FOLLOWER = "pending_incoming_follower" | ||||||
|  |     REJECTED_FOLLOWER = "rejected_follower" | ||||||
|     UNFOLLOW = "unfollow" |     UNFOLLOW = "unfollow" | ||||||
|  |  | ||||||
|     FOLLOW_REQUEST_ACCEPTED = "follow_request_accepted" |     FOLLOW_REQUEST_ACCEPTED = "follow_request_accepted" | ||||||
| @@ -563,6 +565,9 @@ class Notification(Base): | |||||||
|     ) |     ) | ||||||
|     webmention = relationship(Webmention, uselist=False) |     webmention = relationship(Webmention, uselist=False) | ||||||
|  |  | ||||||
|  |     is_accepted = Column(Boolean, nullable=True) | ||||||
|  |     is_rejected = Column(Boolean, nullable=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| outbox_fts = Table( | outbox_fts = Table( | ||||||
|     "outbox_fts", |     "outbox_fts", | ||||||
|   | |||||||
| @@ -22,6 +22,10 @@ | |||||||
|             {%- if notif.notification_type.value == "new_follower" %} |             {%- if notif.notification_type.value == "new_follower" %} | ||||||
|                 {{ notif_actor_action(notif, "followed you") }} |                 {{ notif_actor_action(notif, "followed you") }} | ||||||
|                 {{ utils.display_actor(notif.actor, actors_metadata) }} |                 {{ 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" %} |             {% elif notif.notification_type.value == "unfollow" %} | ||||||
|                 {{ notif_actor_action(notif, "unfollowed you") }} |                 {{ notif_actor_action(notif, "unfollowed you") }} | ||||||
|                 {{ utils.display_actor(notif.actor, actors_metadata) }} |                 {{ utils.display_actor(notif.actor, actors_metadata) }} | ||||||
|   | |||||||
| @@ -33,6 +33,24 @@ | |||||||
| </form> | </form> | ||||||
| {% endmacro %} | {% 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) %} | {% macro admin_like_button(ap_object_id, permalink_id) %} | ||||||
| <form action="{{ request.url_for("admin_actions_like") }}" method="POST"> | <form action="{{ request.url_for("admin_actions_like") }}" method="POST"> | ||||||
|     {{ embed_csrf_token() }} |     {{ embed_csrf_token() }} | ||||||
| @@ -197,7 +215,7 @@ | |||||||
|  |  | ||||||
| {% endmacro %} | {% 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) %} | {% set metadata = actors_metadata.get(actor.ap_id) %} | ||||||
|  |  | ||||||
| {% if not embedded %} | {% if not embedded %} | ||||||
| @@ -243,6 +261,20 @@ | |||||||
|                     <li>{{ admin_block_button(actor) }}</li> |                     <li>{{ admin_block_button(actor) }}</li> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             {% 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> |         </ul> | ||||||
|     </nav> |     </nav> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | from unittest import mock | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| import httpx | import httpx | ||||||
| @@ -32,7 +33,7 @@ def test_inbox_requires_httpsig( | |||||||
|     assert response.json()["detail"] == "Invalid HTTP sig" |     assert response.json()["detail"] == "Invalid HTTP sig" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_inbox_follow_request( | def test_inbox_incoming_follow_request( | ||||||
|     db: Session, |     db: Session, | ||||||
|     client: TestClient, |     client: TestClient, | ||||||
|     respx_mock: respx.MockRouter, |     respx_mock: respx.MockRouter, | ||||||
| @@ -66,11 +67,11 @@ def test_inbox_follow_request( | |||||||
|     run_async(process_next_incoming_activity) |     run_async(process_next_incoming_activity) | ||||||
|  |  | ||||||
|     # And the actor was saved in DB |     # And the actor was saved in DB | ||||||
|     saved_actor = db.query(models.Actor).one() |     saved_actor = db.execute(select(models.Actor)).scalar_one() | ||||||
|     assert saved_actor.ap_id == ra.ap_id |     assert saved_actor.ap_id == ra.ap_id | ||||||
|  |  | ||||||
|     # And the Follow activity was saved in the inbox |     # And the Follow activity was saved in the inbox | ||||||
|     inbox_object = db.query(models.InboxObject).one() |     inbox_object = db.execute(select(models.InboxObject)).scalar_one() | ||||||
|     assert inbox_object.ap_object == follow_activity.ap_object |     assert inbox_object.ap_object == follow_activity.ap_object | ||||||
|  |  | ||||||
|     # And a follower was internally created |     # And a follower was internally created | ||||||
| @@ -80,15 +81,61 @@ def test_inbox_follow_request( | |||||||
|     assert follower.inbox_object_id == inbox_object.id |     assert follower.inbox_object_id == inbox_object.id | ||||||
|  |  | ||||||
|     # And an Accept activity was created in the outbox |     # And an Accept activity was created in the outbox | ||||||
|     outbox_object = db.query(models.OutboxObject).one() |     outbox_object = db.execute(select(models.OutboxObject)).scalar_one() | ||||||
|     assert outbox_object.ap_type == "Accept" |     assert outbox_object.ap_type == "Accept" | ||||||
|     assert outbox_object.activity_object_ap_id == follow_activity.ap_id |     assert outbox_object.activity_object_ap_id == follow_activity.ap_id | ||||||
|  |  | ||||||
|     # And an outgoing activity was created to track the Accept activity delivery |     # And an outgoing activity was created to track the Accept activity delivery | ||||||
|     outgoing_activity = db.query(models.OutgoingActivity).one() |     outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() | ||||||
|     assert outgoing_activity.outbox_object_id == outbox_object.id |     assert outgoing_activity.outbox_object_id == outbox_object.id | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_inbox_incoming_follow_request__manually_approves_followers( | ||||||
|  |     db: Session, | ||||||
|  |     client: TestClient, | ||||||
|  |     respx_mock: respx.MockRouter, | ||||||
|  | ) -> None: | ||||||
|  |     # Given a remote actor | ||||||
|  |     ra = factories.RemoteActorFactory( | ||||||
|  |         base_url="https://example.com", | ||||||
|  |         username="toto", | ||||||
|  |         public_key="pk", | ||||||
|  |     ) | ||||||
|  |     respx_mock.get(ra.ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor)) | ||||||
|  |  | ||||||
|  |     # When receiving a Follow activity | ||||||
|  |     follow_activity = RemoteObject( | ||||||
|  |         factories.build_follow_activity( | ||||||
|  |             from_remote_actor=ra, | ||||||
|  |             for_remote_actor=LOCAL_ACTOR, | ||||||
|  |         ), | ||||||
|  |         ra, | ||||||
|  |     ) | ||||||
|  |     with mock_httpsig_checker(ra): | ||||||
|  |         response = client.post( | ||||||
|  |             "/inbox", | ||||||
|  |             headers={"Content-Type": ap.AS_CTX}, | ||||||
|  |             json=follow_activity.ap_object, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     # Then the server returns a 204 | ||||||
|  |     assert response.status_code == 202 | ||||||
|  |  | ||||||
|  |     with mock.patch("app.boxes.MANUALLY_APPROVES_FOLLOWERS", True): | ||||||
|  |         run_async(process_next_incoming_activity) | ||||||
|  |  | ||||||
|  |     # And the actor was saved in DB | ||||||
|  |     saved_actor = db.execute(select(models.Actor)).scalar_one() | ||||||
|  |     assert saved_actor.ap_id == ra.ap_id | ||||||
|  |  | ||||||
|  |     # And the Follow activity was saved in the inbox | ||||||
|  |     inbox_object = db.execute(select(models.InboxObject)).scalar_one() | ||||||
|  |     assert inbox_object.ap_object == follow_activity.ap_object | ||||||
|  |  | ||||||
|  |     # And no follower was internally created | ||||||
|  |     assert db.scalar(select(func.count(models.Follower.id))) == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_inbox_accept_follow_request( | def test_inbox_accept_follow_request( | ||||||
|     db: Session, |     db: Session, | ||||||
|     client: TestClient, |     client: TestClient, | ||||||
| @@ -133,13 +180,13 @@ def test_inbox_accept_follow_request( | |||||||
|     run_async(process_next_incoming_activity) |     run_async(process_next_incoming_activity) | ||||||
|  |  | ||||||
|     # And the Accept activity was saved in the inbox |     # And the Accept activity was saved in the inbox | ||||||
|     inbox_activity = db.query(models.InboxObject).one() |     inbox_activity = db.execute(select(models.InboxObject)).scalar_one() | ||||||
|     assert inbox_activity.ap_type == "Accept" |     assert inbox_activity.ap_type == "Accept" | ||||||
|     assert inbox_activity.relates_to_outbox_object_id == outbox_object.id |     assert inbox_activity.relates_to_outbox_object_id == outbox_object.id | ||||||
|     assert inbox_activity.actor_id == actor_in_db.id |     assert inbox_activity.actor_id == actor_in_db.id | ||||||
|  |  | ||||||
|     # And a following entry was created internally |     # And a following entry was created internally | ||||||
|     following = db.query(models.Following).one() |     following = db.execute(select(models.Following)).scalar_one() | ||||||
|     assert following.ap_actor_id == actor_in_db.ap_id |     assert following.ap_actor_id == actor_in_db.ap_id | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user