New config item to hide followers/following

This commit is contained in:
Thomas Sileo 2022-09-13 21:03:35 +02:00
parent 567595bb4b
commit b2f268682c
5 changed files with 107 additions and 18 deletions

View file

@ -102,6 +102,9 @@ class Config(pydantic.BaseModel):
emoji: str | None = None
also_known_as: str | None = None
hides_followers: bool = False
hides_following: bool = False
inbox_retention_days: int = 15
# Config items to make tests easier
@ -144,6 +147,8 @@ _SCHEME = "https" if CONFIG.https else "http"
ID = f"{_SCHEME}://{DOMAIN}"
USERNAME = CONFIG.username
MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers
HIDES_FOLLOWERS = CONFIG.hides_followers
HIDES_FOLLOWING = CONFIG.hides_following
PRIVACY_REPLACE = None
if CONFIG.privacy_replace:
PRIVACY_REPLACE = {pr.domain: pr.replace_by for pr in CONFIG.privacy_replace}

View file

@ -403,6 +403,20 @@ async def _build_followx_collection(
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")
async def followers(
request: Request,
@ -413,6 +427,15 @@ async def followers(
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse:
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(
await _build_followx_collection(
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
followers_result = await db_session.scalars(
select(models.Follower)
@ -460,6 +486,15 @@ async def following(
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse:
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(
await _build_followx_collection(
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
following = (
(

View file

@ -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["CSS_HASH"] = config.CSS_HASH
_templates.env.globals["BASE_URL"] = config.BASE_URL
_templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS
_templates.env.globals["HIDES_FOLLOWING"] = config.HIDES_FOLLOWING

View file

@ -36,8 +36,12 @@
{% if articles_count %}
<li>{{ header_link("articles", "Articles") }}</li>
{% endif %}
{% if not HIDES_FOLLOWERS %}
<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>
{% endif %}
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
</ul>
</nav>

View file

@ -1,3 +1,5 @@
from unittest import mock
import pytest
from fastapi.testclient import TestClient
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})
assert response.status_code == 200
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:
@ -40,14 +54,40 @@ def test_followers__html(client, db) -> None:
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:
response = client.get("/following", headers={"Accept": ap.AP_CONTENT_TYPE})
assert response.status_code == 200
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:
response = client.get("/following")
assert response.status_code == 200
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")