mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Start support for authoring articles
This commit is contained in:
		| @@ -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), | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								app/main.py
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								app/main.py
									
									
									
									
									
								
							| @@ -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], | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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)) | ||||||
|   | |||||||
| @@ -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 %} | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								app/templates/articles.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/templates/articles.html
									
									
									
									
									
										Normal 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 %} | ||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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 %} | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user