mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	New config item to hide followers/following
This commit is contained in:
		| @@ -102,6 +102,9 @@ class Config(pydantic.BaseModel): | |||||||
|     emoji: str | None = None |     emoji: str | None = None | ||||||
|     also_known_as: str | None = None |     also_known_as: str | None = None | ||||||
|  |  | ||||||
|  |     hides_followers: bool = False | ||||||
|  |     hides_following: bool = False | ||||||
|  |  | ||||||
|     inbox_retention_days: int = 15 |     inbox_retention_days: int = 15 | ||||||
|  |  | ||||||
|     # Config items to make tests easier |     # Config items to make tests easier | ||||||
| @@ -144,6 +147,8 @@ _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 | MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers | ||||||
|  | HIDES_FOLLOWERS = CONFIG.hides_followers | ||||||
|  | HIDES_FOLLOWING = CONFIG.hides_following | ||||||
| PRIVACY_REPLACE = None | PRIVACY_REPLACE = None | ||||||
| if CONFIG.privacy_replace: | if CONFIG.privacy_replace: | ||||||
|     PRIVACY_REPLACE = {pr.domain: pr.replace_by for pr in CONFIG.privacy_replace} |     PRIVACY_REPLACE = {pr.domain: pr.replace_by for pr in CONFIG.privacy_replace} | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								app/main.py
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								app/main.py
									
									
									
									
									
								
							| @@ -403,6 +403,20 @@ async def _build_followx_collection( | |||||||
|     return collection_page |     return collection_page | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def _empty_followx_collection( | ||||||
|  |     db_session: AsyncSession, | ||||||
|  |     model_cls: Type[models.Following | models.Follower], | ||||||
|  |     path: str, | ||||||
|  | ) -> ap.RawObject: | ||||||
|  |     total_items = await db_session.scalar(select(func.count(model_cls.id))) | ||||||
|  |     return { | ||||||
|  |         "@context": ap.AS_CTX, | ||||||
|  |         "id": ID + path, | ||||||
|  |         "type": "OrderedCollection", | ||||||
|  |         "totalItems": total_items, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/followers") | @app.get("/followers") | ||||||
| async def followers( | async def followers( | ||||||
|     request: Request, |     request: Request, | ||||||
| @@ -413,6 +427,15 @@ async def followers( | |||||||
|     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), |     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), | ||||||
| ) -> ActivityPubResponse | templates.TemplateResponse: | ) -> ActivityPubResponse | templates.TemplateResponse: | ||||||
|     if is_activitypub_requested(request): |     if is_activitypub_requested(request): | ||||||
|  |         if config.HIDES_FOLLOWERS: | ||||||
|  |             return ActivityPubResponse( | ||||||
|  |                 await _empty_followx_collection( | ||||||
|  |                     db_session=db_session, | ||||||
|  |                     model_cls=models.Follower, | ||||||
|  |                     path="/followers", | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|             return ActivityPubResponse( |             return ActivityPubResponse( | ||||||
|                 await _build_followx_collection( |                 await _build_followx_collection( | ||||||
|                     db_session=db_session, |                     db_session=db_session, | ||||||
| @@ -423,6 +446,9 @@ async def followers( | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     if config.HIDES_FOLLOWERS: | ||||||
|  |         raise HTTPException(status_code=404) | ||||||
|  |  | ||||||
|     # We only show the most recent 20 followers on the public website |     # We only show the most recent 20 followers on the public website | ||||||
|     followers_result = await db_session.scalars( |     followers_result = await db_session.scalars( | ||||||
|         select(models.Follower) |         select(models.Follower) | ||||||
| @@ -460,6 +486,15 @@ async def following( | |||||||
|     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), |     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), | ||||||
| ) -> ActivityPubResponse | templates.TemplateResponse: | ) -> ActivityPubResponse | templates.TemplateResponse: | ||||||
|     if is_activitypub_requested(request): |     if is_activitypub_requested(request): | ||||||
|  |         if config.HIDES_FOLLOWING: | ||||||
|  |             return ActivityPubResponse( | ||||||
|  |                 await _empty_followx_collection( | ||||||
|  |                     db_session=db_session, | ||||||
|  |                     model_cls=models.Following, | ||||||
|  |                     path="/following", | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|             return ActivityPubResponse( |             return ActivityPubResponse( | ||||||
|                 await _build_followx_collection( |                 await _build_followx_collection( | ||||||
|                     db_session=db_session, |                     db_session=db_session, | ||||||
| @@ -470,6 +505,9 @@ async def following( | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     if config.HIDES_FOLLOWING: | ||||||
|  |         raise HTTPException(status_code=404) | ||||||
|  |  | ||||||
|     # We only show the most recent 20 follows on the public website |     # We only show the most recent 20 follows on the public website | ||||||
|     following = ( |     following = ( | ||||||
|         ( |         ( | ||||||
|   | |||||||
| @@ -419,3 +419,5 @@ _templates.env.filters["privacy_replace_url"] = privacy_replace.replace_url | |||||||
| _templates.env.globals["JS_HASH"] = config.JS_HASH | _templates.env.globals["JS_HASH"] = config.JS_HASH | ||||||
| _templates.env.globals["CSS_HASH"] = config.CSS_HASH | _templates.env.globals["CSS_HASH"] = config.CSS_HASH | ||||||
| _templates.env.globals["BASE_URL"] = config.BASE_URL | _templates.env.globals["BASE_URL"] = config.BASE_URL | ||||||
|  | _templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS | ||||||
|  | _templates.env.globals["HIDES_FOLLOWING"] = config.HIDES_FOLLOWING | ||||||
|   | |||||||
| @@ -36,8 +36,12 @@ | |||||||
|         {% if articles_count %} |         {% if articles_count %} | ||||||
|             <li>{{ header_link("articles", "Articles") }}</li> |             <li>{{ header_link("articles", "Articles") }}</li> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  |         {% if not HIDES_FOLLOWERS %} | ||||||
|         <li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li> |         <li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li> | ||||||
|  |         {% endif %} | ||||||
|  |         {% if not HIDES_FOLLOWING %} | ||||||
|         <li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li> |         <li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li> | ||||||
|  |         {% endif %} | ||||||
|         <li>{{ header_link("get_remote_follow", "Remote follow") }}</li> |         <li>{{ header_link("get_remote_follow", "Remote follow") }}</li> | ||||||
|     </ul> |     </ul> | ||||||
| </nav> | </nav> | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | from unittest import mock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| from fastapi.testclient import TestClient | from fastapi.testclient import TestClient | ||||||
| from sqlalchemy.orm import Session | from sqlalchemy.orm import Session | ||||||
| @@ -31,7 +33,19 @@ def test_followers__ap(client, db) -> None: | |||||||
|     response = client.get("/followers", headers={"Accept": ap.AP_CONTENT_TYPE}) |     response = client.get("/followers", headers={"Accept": ap.AP_CONTENT_TYPE}) | ||||||
|     assert response.status_code == 200 |     assert response.status_code == 200 | ||||||
|     assert response.headers["content-type"] == ap.AP_CONTENT_TYPE |     assert response.headers["content-type"] == ap.AP_CONTENT_TYPE | ||||||
|     assert response.json()["id"].endswith("/followers") |     json_resp = response.json() | ||||||
|  |     assert json_resp["id"].endswith("/followers") | ||||||
|  |     assert "first" in json_resp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_followers__ap_hides_followers(client, db) -> None: | ||||||
|  |     with mock.patch("app.main.config.HIDES_FOLLOWERS", True): | ||||||
|  |         response = client.get("/followers", headers={"Accept": ap.AP_CONTENT_TYPE}) | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |     assert response.headers["content-type"] == ap.AP_CONTENT_TYPE | ||||||
|  |     json_resp = response.json() | ||||||
|  |     assert json_resp["id"].endswith("/followers") | ||||||
|  |     assert "first" not in json_resp | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_followers__html(client, db) -> None: | def test_followers__html(client, db) -> None: | ||||||
| @@ -40,14 +54,40 @@ def test_followers__html(client, db) -> None: | |||||||
|     assert response.headers["content-type"].startswith("text/html") |     assert response.headers["content-type"].startswith("text/html") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_followers__html_hides_followers(client, db) -> None: | ||||||
|  |     with mock.patch("app.main.config.HIDES_FOLLOWERS", True): | ||||||
|  |         response = client.get("/followers", headers={"Accept": "text/html"}) | ||||||
|  |     assert response.status_code == 404 | ||||||
|  |     assert response.headers["content-type"].startswith("text/html") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_following__ap(client, db) -> None: | def test_following__ap(client, db) -> None: | ||||||
|     response = client.get("/following", headers={"Accept": ap.AP_CONTENT_TYPE}) |     response = client.get("/following", headers={"Accept": ap.AP_CONTENT_TYPE}) | ||||||
|     assert response.status_code == 200 |     assert response.status_code == 200 | ||||||
|     assert response.headers["content-type"] == ap.AP_CONTENT_TYPE |     assert response.headers["content-type"] == ap.AP_CONTENT_TYPE | ||||||
|     assert response.json()["id"].endswith("/following") |     json_resp = response.json() | ||||||
|  |     assert json_resp["id"].endswith("/following") | ||||||
|  |     assert "first" in json_resp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_following__ap_hides_following(client, db) -> None: | ||||||
|  |     with mock.patch("app.main.config.HIDES_FOLLOWING", True): | ||||||
|  |         response = client.get("/following", headers={"Accept": ap.AP_CONTENT_TYPE}) | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |     assert response.headers["content-type"] == ap.AP_CONTENT_TYPE | ||||||
|  |     json_resp = response.json() | ||||||
|  |     assert json_resp["id"].endswith("/following") | ||||||
|  |     assert "first" not in json_resp | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_following__html(client, db) -> None: | def test_following__html(client, db) -> None: | ||||||
|     response = client.get("/following") |     response = client.get("/following") | ||||||
|     assert response.status_code == 200 |     assert response.status_code == 200 | ||||||
|     assert response.headers["content-type"].startswith("text/html") |     assert response.headers["content-type"].startswith("text/html") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_following__html_hides_following(client, db) -> None: | ||||||
|  |     with mock.patch("app.main.config.HIDES_FOLLOWING", True): | ||||||
|  |         response = client.get("/following", headers={"Accept": "text/html"}) | ||||||
|  |     assert response.status_code == 404 | ||||||
|  |     assert response.headers["content-type"].startswith("text/html") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user