mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2025-06-05 21:59:23 +02:00
Compare commits
12 Commits
2.0.0-rc.1
...
2.0.0-rc.2
Author | SHA1 | Date | |
---|---|---|---|
a6321f52d8 | |||
4e1e4d0ea8 | |||
110f7df962 | |||
4c86cd4be3 | |||
df06defbef | |||
b2f268682c | |||
567595bb4b | |||
91b8bb26b7 | |||
bd4d5a004a | |||
04da8725ed | |||
0c7a19749d | |||
2a37034775 |
@ -61,6 +61,10 @@ class ObjectNotFoundError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectUnavailableError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FetchErrorTypeEnum(str, enum.Enum):
|
class FetchErrorTypeEnum(str, enum.Enum):
|
||||||
TIMEOUT = "TIMEOUT"
|
TIMEOUT = "TIMEOUT"
|
||||||
NOT_FOUND = "NOT_FOUND"
|
NOT_FOUND = "NOT_FOUND"
|
||||||
@ -167,6 +171,8 @@ async def fetch(
|
|||||||
# Special handling for deleted object
|
# Special handling for deleted object
|
||||||
if resp.status_code == 410:
|
if resp.status_code == 410:
|
||||||
raise ObjectIsGoneError(f"{url} is gone")
|
raise ObjectIsGoneError(f"{url} is gone")
|
||||||
|
elif resp.status_code in [401, 403]:
|
||||||
|
raise ObjectUnavailableError(f"not allowed to fetch {url}")
|
||||||
elif resp.status_code == 404:
|
elif resp.status_code == 404:
|
||||||
raise ObjectNotFoundError(f"{url} not found")
|
raise ObjectNotFoundError(f"{url} not found")
|
||||||
|
|
||||||
|
@ -85,6 +85,8 @@ async def get_lookup(
|
|||||||
error = ap.FetchErrorTypeEnum.TIMEOUT
|
error = ap.FetchErrorTypeEnum.TIMEOUT
|
||||||
except (ap.ObjectNotFoundError, ap.ObjectIsGoneError):
|
except (ap.ObjectNotFoundError, ap.ObjectIsGoneError):
|
||||||
error = ap.FetchErrorTypeEnum.NOT_FOUND
|
error = ap.FetchErrorTypeEnum.NOT_FOUND
|
||||||
|
except (ap.ObjectUnavailableError):
|
||||||
|
error = ap.FetchErrorTypeEnum.UNAUHTORIZED
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(f"Failed to lookup {query}")
|
logger.exception(f"Failed to lookup {query}")
|
||||||
error = ap.FetchErrorTypeEnum.INTERNAL_ERROR
|
error = ap.FetchErrorTypeEnum.INTERNAL_ERROR
|
||||||
|
34
app/boxes.py
34
app/boxes.py
@ -1422,7 +1422,8 @@ async def _handle_update_activity(
|
|||||||
updated_actor = RemoteActor(wrapped_object)
|
updated_actor = RemoteActor(wrapped_object)
|
||||||
if (
|
if (
|
||||||
from_actor.ap_id != updated_actor.ap_id
|
from_actor.ap_id != updated_actor.ap_id
|
||||||
or from_actor.ap_type != updated_actor.ap_type
|
or ap.as_list(from_actor.ap_type)[0] not in ap.ACTOR_TYPES
|
||||||
|
or ap.as_list(updated_actor.ap_type)[0] not in ap.ACTOR_TYPES
|
||||||
or from_actor.handle != updated_actor.handle
|
or from_actor.handle != updated_actor.handle
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -1783,6 +1784,12 @@ async def _handle_announce_activity(
|
|||||||
announced_raw_object = await ap.fetch(
|
announced_raw_object = await ap.fetch(
|
||||||
announce_activity.activity_object_ap_id
|
announce_activity.activity_object_ap_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Some software return objects wrapped in a Create activity (like
|
||||||
|
# python-federation)
|
||||||
|
if ap.as_list(announced_raw_object["type"])[0] == "Create":
|
||||||
|
announced_raw_object = await ap.get_object(announced_raw_object)
|
||||||
|
|
||||||
announced_actor = await fetch_actor(
|
announced_actor = await fetch_actor(
|
||||||
db_session, ap.get_actor_id(announced_raw_object)
|
db_session, ap.get_actor_id(announced_raw_object)
|
||||||
)
|
)
|
||||||
@ -1844,6 +1851,7 @@ async def _process_transient_object(
|
|||||||
if ap_type in ["Add", "Remove"]:
|
if ap_type in ["Add", "Remove"]:
|
||||||
logger.info(f"Dropping unsupported {ap_type} object")
|
logger.info(f"Dropping unsupported {ap_type} object")
|
||||||
else:
|
else:
|
||||||
|
# FIXME(ts): handle transient create
|
||||||
logger.warning(f"Received unknown {ap_type} object")
|
logger.warning(f"Received unknown {ap_type} object")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -1854,6 +1862,28 @@ async def save_to_inbox(
|
|||||||
raw_object: ap.RawObject,
|
raw_object: ap.RawObject,
|
||||||
sent_by_ap_actor_id: str,
|
sent_by_ap_actor_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# Special case for server sending the actor as a payload (like python-federation)
|
||||||
|
if ap.as_list(raw_object["type"])[0] in ap.ACTOR_TYPES:
|
||||||
|
if ap.get_id(raw_object) == sent_by_ap_actor_id:
|
||||||
|
updated_actor = RemoteActor(raw_object)
|
||||||
|
|
||||||
|
try:
|
||||||
|
actor = await fetch_actor(db_session, sent_by_ap_actor_id)
|
||||||
|
except ap.ObjectNotFoundError:
|
||||||
|
logger.warning("Actor not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the actor
|
||||||
|
actor.ap_actor = updated_actor.ap_actor
|
||||||
|
await db_session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Reveived an actor payload {raw_object} from " f"{sent_by_ap_actor_id}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actor = await fetch_actor(db_session, ap.get_id(raw_object["actor"]))
|
actor = await fetch_actor(db_session, ap.get_id(raw_object["actor"]))
|
||||||
except ap.ObjectNotFoundError:
|
except ap.ObjectNotFoundError:
|
||||||
@ -1867,7 +1897,7 @@ async def save_to_inbox(
|
|||||||
logger.warning(f"Server {actor.server} is blocked")
|
logger.warning(f"Server {actor.server} is blocked")
|
||||||
return
|
return
|
||||||
|
|
||||||
if "id" not in raw_object:
|
if "id" not in raw_object or not raw_object["id"]:
|
||||||
await _process_transient_object(db_session, raw_object, actor)
|
await _process_transient_object(db_session, raw_object, actor)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -115,11 +115,8 @@ async def _get_public_key(db_session: AsyncSession, key_id: str) -> Key:
|
|||||||
# might race to fetch each other key
|
# might race to fetch each other key
|
||||||
try:
|
try:
|
||||||
actor = await ap.fetch(key_id, disable_httpsig=True)
|
actor = await ap.fetch(key_id, disable_httpsig=True)
|
||||||
except httpx.HTTPStatusError as http_err:
|
except ap.ObjectUnavailableError:
|
||||||
if http_err.response.status_code in [401, 403]:
|
|
||||||
actor = await ap.fetch(key_id, disable_httpsig=False)
|
actor = await ap.fetch(key_id, disable_httpsig=False)
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if actor["type"] == "Key":
|
if actor["type"] == "Key":
|
||||||
# The Key is not embedded in the Person
|
# The Key is not embedded in the Person
|
||||||
|
@ -26,7 +26,7 @@ async def new_ap_incoming_activity(
|
|||||||
raw_object: ap.RawObject,
|
raw_object: ap.RawObject,
|
||||||
) -> models.IncomingActivity | None:
|
) -> models.IncomingActivity | None:
|
||||||
ap_id: str
|
ap_id: str
|
||||||
if "id" not in raw_object:
|
if "id" not in raw_object or ap.as_list(raw_object["type"])[0] in ap.ACTOR_TYPES:
|
||||||
if "@context" not in raw_object:
|
if "@context" not in raw_object:
|
||||||
logger.warning(f"Dropping invalid object: {raw_object}")
|
logger.warning(f"Dropping invalid object: {raw_object}")
|
||||||
return None
|
return None
|
||||||
|
@ -38,4 +38,9 @@ async def lookup(db_session: AsyncSession, query: str) -> Actor | RemoteObject:
|
|||||||
if ap.as_list(ap_obj["type"])[0] in ap.ACTOR_TYPES:
|
if ap.as_list(ap_obj["type"])[0] in ap.ACTOR_TYPES:
|
||||||
return RemoteActor(ap_obj)
|
return RemoteActor(ap_obj)
|
||||||
else:
|
else:
|
||||||
|
# Some software return objects wrapped in a Create activity (like
|
||||||
|
# python-federation)
|
||||||
|
if ap.as_list(ap_obj["type"])[0] == "Create":
|
||||||
|
ap_obj = await ap.get_object(ap_obj)
|
||||||
|
|
||||||
return await RemoteObject.from_raw_object(ap_obj)
|
return await RemoteObject.from_raw_object(ap_obj)
|
||||||
|
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 and not is_current_user_admin(request):
|
||||||
|
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 and not is_current_user_admin(request):
|
||||||
|
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 or is_admin %}
|
||||||
<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 or is_admin %}
|
||||||
<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>
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
<div class="box error-box">
|
<div class="box error-box">
|
||||||
{% if error.value == "NOT_FOUND" %}
|
{% if error.value == "NOT_FOUND" %}
|
||||||
<p>The remote object is unavailable.</p>
|
<p>The remote object is unavailable.</p>
|
||||||
|
{% elif error.value == "UNAUTHORIZED" %}
|
||||||
|
<p>Missing permissions to fetch the remote object.</p>
|
||||||
{% elif error.value == "TIMEOUT" %}
|
{% elif error.value == "TIMEOUT" %}
|
||||||
<p>Lookup timed out, please try refreshing the page.</p>
|
<p>Lookup timed out, please try refreshing the page.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -46,7 +46,7 @@ async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload:
|
|||||||
width = None
|
width = None
|
||||||
height = None
|
height = None
|
||||||
|
|
||||||
if f.content_type.startswith("image"):
|
if f.content_type.startswith("image") and not f.content_type == "image/gif":
|
||||||
with Image.open(f.file) as _original_image:
|
with Image.open(f.file) as _original_image:
|
||||||
# Fix image orientation (as we will remove the info from the EXIF
|
# Fix image orientation (as we will remove the info from the EXIF
|
||||||
# metadata)
|
# metadata)
|
||||||
|
@ -81,6 +81,9 @@ async def external_urls(
|
|||||||
soup = BeautifulSoup(ro.content, "html5lib")
|
soup = BeautifulSoup(ro.content, "html5lib")
|
||||||
for link in soup.find_all("a"):
|
for link in soup.find_all("a"):
|
||||||
h = link.get("href")
|
h = link.get("href")
|
||||||
|
if not h:
|
||||||
|
continue
|
||||||
|
|
||||||
ph = urlparse(h)
|
ph = urlparse(h)
|
||||||
mimetype, _ = mimetypes.guess_type(h)
|
mimetype, _ = mimetypes.guess_type(h)
|
||||||
if (
|
if (
|
||||||
|
@ -58,6 +58,26 @@ manually_approves_followers = true
|
|||||||
|
|
||||||
The default value is `false`.
|
The default value is `false`.
|
||||||
|
|
||||||
|
### Hiding followers
|
||||||
|
|
||||||
|
If you wish to hide your followers, add this config item to `profile.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
hides_followers = true
|
||||||
|
```
|
||||||
|
|
||||||
|
The default value is `false`.
|
||||||
|
|
||||||
|
### Hiding following
|
||||||
|
|
||||||
|
If you wish to hide your following, add this config item to `profile.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
hides_following = true
|
||||||
|
```
|
||||||
|
|
||||||
|
The default value is `false`.
|
||||||
|
|
||||||
### Privacy replace
|
### Privacy replace
|
||||||
|
|
||||||
You can define domain to be rewrited to more "privacy friendly" alternatives, like [Invidious](https://invidious.io/)
|
You can define domain to be rewrited to more "privacy friendly" alternatives, like [Invidious](https://invidious.io/)
|
||||||
@ -241,6 +261,8 @@ If you want to move followers from your existing account, ensure it is supported
|
|||||||
|
|
||||||
For [Mastodon you can look at Moving or leaving accounts](https://docs.joinmastodon.org/user/moving/).
|
For [Mastodon you can look at Moving or leaving accounts](https://docs.joinmastodon.org/user/moving/).
|
||||||
|
|
||||||
|
If you wish to move **to** another instance, see [Moving to another instance](/user_guide.html#moving-to-another-instance).
|
||||||
|
|
||||||
First you need to grab the "ActivityPub actor URL" for your existing account:
|
First you need to grab the "ActivityPub actor URL" for your existing account:
|
||||||
|
|
||||||
### Python edition
|
### Python edition
|
||||||
@ -323,6 +345,8 @@ If you want to migrate to another instance, you have the ability to move your ex
|
|||||||
|
|
||||||
Your new account should reference the existing one, refer to your software configuration (for example [Moving or leaving accounts from the Mastodon doc](https://docs.joinmastodon.org/user/moving/)).
|
Your new account should reference the existing one, refer to your software configuration (for example [Moving or leaving accounts from the Mastodon doc](https://docs.joinmastodon.org/user/moving/)).
|
||||||
|
|
||||||
|
If you wish to move **from** another instance, see [Moving from another instance](/user_guide.html#moving-from-another-instance).
|
||||||
|
|
||||||
Execute the Move task:
|
Execute the Move task:
|
||||||
|
|
||||||
#### Python edition
|
#### Python edition
|
||||||
|
17
tasks.py
17
tasks.py
@ -264,7 +264,7 @@ def move_to(ctx, moved_to):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await send_move(db_session, moved_to)
|
await send_move(db_session, new_actor.ap_id)
|
||||||
|
|
||||||
print("Done")
|
print("Done")
|
||||||
|
|
||||||
@ -312,3 +312,18 @@ def yunohost_config(
|
|||||||
summary=summary,
|
summary=summary,
|
||||||
password=password,
|
password=password,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def reset_password(ctx):
|
||||||
|
# type: (Context) -> None
|
||||||
|
import bcrypt
|
||||||
|
from prompt_toolkit import prompt
|
||||||
|
|
||||||
|
new_password = bcrypt.hashpw(
|
||||||
|
prompt("New admin password: ", is_password=True).encode(), bcrypt.gensalt()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Update data/profile.toml with:")
|
||||||
|
print(f'admin_password = "{new_password}"')
|
||||||
|
@ -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