Start support for authoring articles

main
Thomas Sileo 2022-07-25 22:51:53 +02:00
parent e363ae2802
commit 24f3f94056
10 changed files with 132 additions and 8 deletions

View File

@ -661,6 +661,7 @@ async def admin_actions_new(
is_sensitive: bool = Form(False), is_sensitive: bool = Form(False),
visibility: str = Form(), visibility: str = Form(),
poll_type: str | None = Form(None), poll_type: str | None = Form(None),
name: str | None = Form(None),
csrf_check: None = Depends(verify_csrf_token), csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse: ) -> RedirectResponse:
@ -687,6 +688,8 @@ async def admin_actions_new(
raise ValueError("Question must have at least 2 answers") raise ValueError("Question must have at least 2 answers")
poll_duration_in_minutes = int(raw_form_data["poll_duration"]) poll_duration_in_minutes = int(raw_form_data["poll_duration"])
elif name:
ap_type = "Article"
public_id = await boxes.send_create( public_id = await boxes.send_create(
db_session, db_session,
@ -700,6 +703,7 @@ async def admin_actions_new(
poll_type=poll_type, poll_type=poll_type,
poll_answers=poll_answers, poll_answers=poll_answers,
poll_duration_in_minutes=poll_duration_in_minutes, poll_duration_in_minutes=poll_duration_in_minutes,
name=name,
) )
return RedirectResponse( return RedirectResponse(
request.url_for("outbox_by_public_id", public_id=public_id), request.url_for("outbox_by_public_id", public_id=public_id),

View File

@ -297,6 +297,7 @@ async def send_create(
poll_type: str | None = None, poll_type: str | None = None,
poll_answers: list[str] | None = None, poll_answers: list[str] | None = None,
poll_duration_in_minutes: int | None = None, poll_duration_in_minutes: int | None = None,
name: str | None = None,
) -> str: ) -> str:
note_id = allocate_outbox_id() note_id = allocate_outbox_id()
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z") published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
@ -367,6 +368,11 @@ async def send_create(
for answer in poll_answers for answer in poll_answers
], ],
} }
elif ap_type == "Article":
if not name:
raise ValueError("Article must have a name")
extra_obj_attrs = {"name": name}
obj = { obj = {
"@context": ap.AS_EXTENDED_CTX, "@context": ap.AS_EXTENDED_CTX,

View File

@ -215,6 +215,7 @@ async def index(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.is_hidden_from_homepage.is_(False), models.OutboxObject.is_hidden_from_homepage.is_(False),
models.OutboxObject.ap_type != "Article",
) )
q = select(models.OutboxObject).where(*where) q = select(models.OutboxObject).where(*where)
total_count = await db_session.scalar( total_count = await db_session.scalar(
@ -257,6 +258,51 @@ async def index(
) )
@app.get("/articles")
async def articles(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
page: int | None = None,
) -> templates.TemplateResponse | ActivityPubResponse:
# TODO: special ActivityPub collection for Article
where = (
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.is_hidden_from_homepage.is_(False),
models.OutboxObject.ap_type == "Article",
)
q = select(models.OutboxObject).where(*where)
outbox_objects_result = await db_session.scalars(
q.options(
joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
),
joinedload(models.OutboxObject.relates_to_inbox_object).options(
joinedload(models.InboxObject.actor),
),
joinedload(models.OutboxObject.relates_to_outbox_object).options(
joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
),
),
).order_by(models.OutboxObject.ap_published_at.desc())
)
outbox_objects = outbox_objects_result.unique().all()
return await templates.render_template(
db_session,
request,
"articles.html",
{
"request": request,
"objects": outbox_objects,
},
)
async def _build_followx_collection( async def _build_followx_collection(
db_session: AsyncSession, db_session: AsyncSession,
model_cls: Type[models.Following | models.Follower], model_cls: Type[models.Following | models.Follower],

View File

@ -142,6 +142,15 @@ footer {
max-width: 50px; max-width: 50px;
} }
} }
#articles {
list-style-type: none;
margin: 30px 0;
padding: 0 20px;
li {
display: block;
}
}
#notifications, #followers, #following { #notifications, #followers, #following {
ul { ul {
list-style-type: none; list-style-type: none;

View File

@ -109,6 +109,14 @@ async def render_template(
) )
if is_admin if is_admin
else 0, else 0,
"articles_count": await db_session.scalar(
select(func.count(models.OutboxObject.id)).where(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.is_hidden_from_homepage.is_(False),
models.OutboxObject.ap_type == "Article",
)
),
"local_actor": LOCAL_ACTOR, "local_actor": LOCAL_ACTOR,
"followers_count": await db_session.scalar( "followers_count": await db_session.scalar(
select(func.count(models.Follower.id)) select(func.count(models.Follower.id))

View File

@ -15,7 +15,7 @@
<div class="box"> <div class="box">
<nav class="flexbox"> <nav class="flexbox">
<ul> <ul>
{% for ap_type in ["Note", "Question"] %} {% for ap_type in ["Note", "Article", "Question"] %}
<li><a href="?type={{ ap_type }}" {% if request.query_params.get("type", "Note") == ap_type %}class="active"{% endif %}> <li><a href="?type={{ ap_type }}" {% if request.query_params.get("type", "Note") == ap_type %}class="active"{% endif %}>
{{ ap_type }} {{ ap_type }}
</a> </a>
@ -35,6 +35,13 @@
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
{% if request.query_params.type == "Article" %}
<p>
<input type="text" style="width:95%" name="name" placeholder="Title">
</p>
{% endif %}
{% for emoji in emojis %} {% for emoji in emojis %}
<span class="ji">{{ emoji | emojify(True) | safe }}</span> <span class="ji">{{ emoji | emojify(True) | safe }}</span>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,20 @@
{%- import "utils.html" as utils with context -%}
{% extends "layout.html" %}
{% block head %}
<title>{{ local_actor.display_name }}'s articles</title>
{% endblock %}
{% block content %}
{% include "header.html" %}
<ul class="h-feed" id="articles">
<data class="p-name" value="{{ local_actor.display_name}}'s articles"></data>
{% for outbox_object in objects %}
<li>
<span class="muted" style="padding-right:10px;">{{ outbox_object.ap_published_at.strftime("%Y-%m-%d") }}</span> <a href="{{ outbox_object.url }}">{{ outbox_object.name }}</a>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -22,6 +22,9 @@
<nav class="flexbox"> <nav class="flexbox">
<ul> <ul>
<li>{{ header_link("index", "Notes") }}</li> <li>{{ header_link("index", "Notes") }}</li>
{% if articles_count %}
<li>{{ header_link("articles", "Articles") }}</li>
{% endif %}
<li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li> <li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li>
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li> <li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li> <li>{{ header_link("get_remote_follow", "Remote follow") }}</li>

View File

@ -4,14 +4,14 @@
{% block head %} {% block head %}
{% if outbox_object %} {% if outbox_object %}
{% set excerpt = outbox_object.content | html2text | trim | truncate(50) %} {% set excerpt = outbox_object.content | html2text | trim | truncate(50) %}
<title>{{ local_actor.display_name }}: "{{ excerpt }}"</title> <title>{% if outbox_object.name %}{{ outbox_object.name }}{% else %}{{ local_actor.display_name }}: "{{ excerpt }}"{% endif %}</title>
<link rel="webmention" href="{{ url_for("webmention_endpoint") }}"> <link rel="webmention" href="{{ url_for("webmention_endpoint") }}">
<link rel="alternate" href="{{ request.url }}" type="application/activity+json"> <link rel="alternate" href="{{ request.url }}" type="application/activity+json">
<meta name="description" content="{{ excerpt }}"> <meta name="description" content="{{ excerpt }}">
<meta content="article" property="og:type" /> <meta content="article" property="og:type" />
<meta content="{{ outbox_object.url }}" property="og:url" /> <meta content="{{ outbox_object.url }}" property="og:url" />
<meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" /> <meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" />
<meta content="Note" property="og:title" /> <meta content="{% if outbox_object.name %}{{ name }}{% else %}Note{% endif %}" property="og:title" />
<meta content="{{ excerpt }}" property="og:description" /> <meta content="{{ excerpt }}" property="og:description" />
<meta content="{{ local_actor.icon_url }}" property="og:image" /> <meta content="{{ local_actor.icon_url }}" property="og:image" />
<meta content="summary" property="twitter:card" /> <meta content="summary" property="twitter:card" />
@ -24,7 +24,7 @@
{% macro display_replies_tree(replies_tree_node) %} {% macro display_replies_tree(replies_tree_node) %}
{% if replies_tree_node.is_requested %} {% if replies_tree_node.is_requested %}
{{ utils.display_object(replies_tree_node.ap_object, likes=likes, shares=shares, webmentions=webmentions, expanded=not replies_tree_node.is_root) }} {{ utils.display_object(replies_tree_node.ap_object, likes=likes, shares=shares, webmentions=webmentions, expanded=not replies_tree_node.is_root, is_object_page=True) }}
{% else %} {% else %}
{{ utils.display_object(replies_tree_node.ap_object) }} {{ utils.display_object(replies_tree_node.ap_object) }}
{% endif %} {% endif %}

View File

@ -263,10 +263,20 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False, actors_metadata={}) %} {% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False, actors_metadata={}, is_object_page=False) %}
{% set is_article_mode = object.is_from_outbox and object.ap_type == "Article" and is_object_page %}
{% if object.ap_type in ["Note", "Article", "Video", "Page", "Question"] %} {% if object.ap_type in ["Note", "Article", "Video", "Page", "Question"] %}
<div class="ap-object {% if expanded %}ap-object-expanded {% endif %}h-entry" id="{{ object.permalink_id }}"> <div class="ap-object {% if expanded %}ap-object-expanded {% endif %}h-entry" id="{{ object.permalink_id }}">
{% if is_article_mode %}
<data class="h-card">
<data class="u-photo" value="{{ local_actor.icon_url }}"></data>
<data class="u-url" value="{{ local_actor.url}}"></data>
<data class="p-name" value="{{ local_actor.handle }}"></data>
</data>
{% else %}
{{ display_actor(object.actor, actors_metadata, embedded=True) }} {{ display_actor(object.actor, actors_metadata, embedded=True) }}
{% endif %}
{% if object.in_reply_to %} {% if object.in_reply_to %}
<a href="{% if is_admin %}{{ url_for("get_lookup") }}?query={% endif %}{{ object.in_reply_to }}" title="{{ object.in_reply_to }}" class="in-reply-to" rel="nofollow"> <a href="{% if is_admin %}{{ url_for("get_lookup") }}?query={% endif %}{{ object.in_reply_to }}" title="{{ object.in_reply_to }}" class="in-reply-to" rel="nofollow">
@ -274,6 +284,15 @@
</a> </a>
{% endif %} {% endif %}
{% if object.ap_type == "Article" %}
<h2 class="p-name" style="margin-top:0;">{{ object.name }}</h2>
{% endif %}
{% if is_article_mode %}
<time class="dt-published muted" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at.strftime("%b %d, %Y") }}</time>
{% endif %}
{% if object.summary %} {% if object.summary %}
<p class="p-summary">{{ object.summary | clean_html(object) | safe }}</p> <p class="p-summary">{{ object.summary | clean_html(object) | safe }}</p>
{% endif %} {% endif %}
@ -352,9 +371,11 @@
<li> <li>
<div><a href="{{ object.url }}"{% if object.is_from_inbox %} rel="nofollow"{% endif %} class="object-permalink u-url u-uid">permalink</a></div> <div><a href="{{ object.url }}"{% if object.is_from_inbox %} rel="nofollow"{% endif %} class="object-permalink u-url u-uid">permalink</a></div>
</li> </li>
<li> {% if not is_article_mode %}
<time class="dt-published" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at | timeago }}</time> <li>
</li> <time class="dt-published" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at | timeago }}</time>
</li>
{% endif %}
{% if object.ap_type == "Question" %} {% if object.ap_type == "Question" %}
{% set endAt = object.ap_object.endTime | parse_datetime %} {% set endAt = object.ap_object.endTime | parse_datetime %}
<li> <li>