Improved audience handling and outbox ALC

main
Thomas Sileo 2022-06-26 18:40:25 +02:00
parent 4bf54c7040
commit 6d756fb8c4
6 changed files with 41 additions and 15 deletions

View File

@ -119,7 +119,7 @@ def admin_new(
{ {
"in_reply_to_object": in_reply_to_object, "in_reply_to_object": in_reply_to_object,
"content": content, "content": content,
"visibility_enum": [ "visibility_choices": [
(v.name, ap.VisibilityEnum.get_display_name(v)) (v.name, ap.VisibilityEnum.get_display_name(v))
for v in ap.VisibilityEnum for v in ap.VisibilityEnum
], ],

View File

@ -15,6 +15,7 @@ from app import activitypub as ap
from app import config from app import config
from app import models from app import models
from app.actor import LOCAL_ACTOR from app.actor import LOCAL_ACTOR
from app.actor import Actor
from app.actor import RemoteActor from app.actor import RemoteActor
from app.actor import fetch_actor from app.actor import fetch_actor
from app.actor import save_actor from app.actor import save_actor
@ -270,6 +271,8 @@ def send_create(
elif visibility == ap.VisibilityEnum.DIRECT: elif visibility == ap.VisibilityEnum.DIRECT:
to = mentioned_actors to = mentioned_actors
cc = [] cc = []
else:
raise ValueError(f"Unhandled visibility {visibility}")
note = { note = {
"@context": ap.AS_CTX, "@context": ap.AS_CTX,
@ -329,8 +332,7 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
# If we got a local collection, assume it's a collection of actors # If we got a local collection, assume it's a collection of actors
if r.startswith(BASE_URL): if r.startswith(BASE_URL):
for raw_actor in fetch_collection(db, r): for actor in fetch_actor_collection(db, r):
actor = RemoteActor(raw_actor)
recipients.add(actor.shared_inbox_url or actor.inbox_url) recipients.add(actor.shared_inbox_url or actor.inbox_url)
continue continue
@ -757,15 +759,15 @@ def public_outbox_objects_count(db: Session) -> int:
) )
def fetch_collection(db: Session, url: str) -> list[ap.RawObject]: def fetch_actor_collection(db: Session, url: str) -> list[Actor]:
if url.startswith(config.BASE_URL): if url.startswith(config.BASE_URL):
if url == config.BASE_URL + "/followers": if url == config.BASE_URL + "/followers":
q = db.query(models.Follower).options(joinedload(models.Follower.actor)) q = db.query(models.Follower).options(joinedload(models.Follower.actor))
return [follower.actor.ap_actor for follower in q.all()] return [follower.actor for follower in q.all()]
else: else:
raise ValueError(f"internal collection for {url}) not supported") raise ValueError(f"internal collection for {url}) not supported")
return ap.parse_collection(url) return [RemoteActor(actor) for actor in ap.parse_collection(url)]
@dataclass @dataclass

View File

@ -408,14 +408,33 @@ def featured(
) )
def _check_outbox_object_acl(
db: Session, ap_object: models.OutboxObject, httpsig_info: httpsig.HTTPSigInfo
) -> None:
if ap_object.visibility in [
ap.VisibilityEnum.PUBLIC,
ap.VisibilityEnum.UNLISTED,
]:
return None
elif ap_object.visibility == ap.VisibilityEnum.FOLLOWERS_ONLY:
followers = boxes.fetch_actor_collection(db, BASE_URL + "/followers")
if httpsig_info.signed_by_ap_actor_id in [actor.ap_id for actor in followers]:
return None
elif ap_object.visibility == ap.VisibilityEnum.DIRECT:
audience = ap_object.ap_object.get("to", []) + ap_object.ap_object.get("cc", [])
if httpsig_info.signed_by_ap_actor_id in audience:
return None
raise HTTPException(status_code=404)
@app.get("/o/{public_id}") @app.get("/o/{public_id}")
def outbox_by_public_id( def outbox_by_public_id(
public_id: str, public_id: str,
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse: ) -> ActivityPubResponse | templates.TemplateResponse:
# TODO: ACL?
maybe_object = ( maybe_object = (
db.query(models.OutboxObject) db.query(models.OutboxObject)
.options( .options(
@ -432,6 +451,8 @@ def outbox_by_public_id(
if not maybe_object: if not maybe_object:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
_check_outbox_object_acl(db, maybe_object, httpsig_info)
if is_activitypub_requested(request): if is_activitypub_requested(request):
return ActivityPubResponse(maybe_object.ap_object) return ActivityPubResponse(maybe_object.ap_object)
@ -452,9 +473,8 @@ def outbox_by_public_id(
def outbox_activity_by_public_id( def outbox_activity_by_public_id(
public_id: str, public_id: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse: ) -> ActivityPubResponse:
# TODO: ACL?
maybe_object = ( maybe_object = (
db.query(models.OutboxObject) db.query(models.OutboxObject)
.filter( .filter(
@ -466,6 +486,8 @@ def outbox_activity_by_public_id(
if not maybe_object: if not maybe_object:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
_check_outbox_object_acl(db, maybe_object, httpsig_info)
return ActivityPubResponse(ap.wrap_object(maybe_object.ap_object)) return ActivityPubResponse(ap.wrap_object(maybe_object.ap_object))

View File

@ -14,6 +14,7 @@ from loguru import logger
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette.templating import _TemplateResponse as TemplateResponse from starlette.templating import _TemplateResponse as TemplateResponse
from app import activitypub as ap
from app import models from app import models
from app.actor import LOCAL_ACTOR from app.actor import LOCAL_ACTOR
from app.ap_object import Attachment from app.ap_object import Attachment
@ -84,6 +85,7 @@ def render_template(
"is_admin": is_admin, "is_admin": is_admin,
"csrf_token": generate_csrf_token() if is_admin else None, "csrf_token": generate_csrf_token() if is_admin else None,
"highlight_css": HIGHLIGHT_CSS, "highlight_css": HIGHLIGHT_CSS,
"visibility_enum": ap.VisibilityEnum,
"notifications_count": db.query(models.Notification) "notifications_count": db.query(models.Notification)
.filter(models.Notification.is_new.is_(True)) .filter(models.Notification.is_new.is_(True))
.count() .count()

View File

@ -12,8 +12,8 @@
{{ utils.embed_redirect_url() }} {{ utils.embed_redirect_url() }}
<p> <p>
<select name="visibility"> <select name="visibility">
{% for (k, v) in visibility_enum %} {% for (k, v) in visibility_choices %}
<option value="{{ k }}">{{ v }}</option> <option value="{{ k }}" {% if in_reply_to_object and in_reply_to_object.visibility.name == k %}selected{% endif %}>{{ v }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>

View File

@ -60,12 +60,12 @@
</form> </form>
{% endmacro %} {% endmacro %}
{% macro admin_announce_button(ap_object_id) %} {% macro admin_announce_button(ap_object_id, disabled=False) %}
<form action="{{ request.url_for("admin_actions_announce") }}" method="POST"> <form action="{{ request.url_for("admin_actions_announce") }}" method="POST">
{{ embed_csrf_token() }} {{ embed_csrf_token() }}
{{ embed_redirect_url() }} {{ embed_redirect_url() }}
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}"> <input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
<input type="submit" value="Share"> <input type="submit" value="Share" {% if disabled %}title="Cannot share non-public content" disabled{% endif %}>
</form> </form>
{% endmacro %} {% endmacro %}
@ -253,7 +253,7 @@
{% if object.announced_via_outbox_object_ap_id %} {% if object.announced_via_outbox_object_ap_id %}
{{ admin_undo_button(object.liked_via_outbox_object_ap_id, "Unshare") }} {{ admin_undo_button(object.liked_via_outbox_object_ap_id, "Unshare") }}
{% else %} {% else %}
{{ admin_announce_button(object.ap_id) }} {{ admin_announce_button(object.ap_id, disabled=object.visibility not in [visibility_enum.PUBLIC, visibility_enum.UNLISTED]) }}
{% endif %} {% endif %}
</div> </div>
<div class="bar-item"> <div class="bar-item">