New config item to hide followers/following
parent
567595bb4b
commit
b2f268682c
|
@ -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}
|
||||||
|
|
70
app/main.py
70
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,15 +427,27 @@ 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):
|
||||||
return ActivityPubResponse(
|
if config.HIDES_FOLLOWERS:
|
||||||
await _build_followx_collection(
|
return ActivityPubResponse(
|
||||||
db_session=db_session,
|
await _empty_followx_collection(
|
||||||
model_cls=models.Follower,
|
db_session=db_session,
|
||||||
path="/followers",
|
model_cls=models.Follower,
|
||||||
page=page,
|
path="/followers",
|
||||||
next_cursor=next_cursor,
|
)
|
||||||
)
|
)
|
||||||
)
|
else:
|
||||||
|
return ActivityPubResponse(
|
||||||
|
await _build_followx_collection(
|
||||||
|
db_session=db_session,
|
||||||
|
model_cls=models.Follower,
|
||||||
|
path="/followers",
|
||||||
|
page=page,
|
||||||
|
next_cursor=next_cursor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
@ -460,15 +486,27 @@ 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):
|
||||||
return ActivityPubResponse(
|
if config.HIDES_FOLLOWING:
|
||||||
await _build_followx_collection(
|
return ActivityPubResponse(
|
||||||
db_session=db_session,
|
await _empty_followx_collection(
|
||||||
model_cls=models.Following,
|
db_session=db_session,
|
||||||
path="/following",
|
model_cls=models.Following,
|
||||||
page=page,
|
path="/following",
|
||||||
next_cursor=next_cursor,
|
)
|
||||||
)
|
)
|
||||||
)
|
else:
|
||||||
|
return ActivityPubResponse(
|
||||||
|
await _build_followx_collection(
|
||||||
|
db_session=db_session,
|
||||||
|
model_cls=models.Following,
|
||||||
|
path="/following",
|
||||||
|
page=page,
|
||||||
|
next_cursor=next_cursor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
Loading…
Reference in New Issue