mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Tweak design and AP tag supports
This commit is contained in:
		
							
								
								
									
										29
									
								
								app/main.py
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								app/main.py
									
									
									
									
									
								
							| @@ -551,15 +551,35 @@ async def tag_by_name( | |||||||
|     db_session: AsyncSession = Depends(get_db_session), |     db_session: AsyncSession = Depends(get_db_session), | ||||||
|     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), |     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), | ||||||
| ) -> ActivityPubResponse | templates.TemplateResponse: | ) -> ActivityPubResponse | templates.TemplateResponse: | ||||||
|  |     where = [ | ||||||
|  |         models.TaggedOutboxObject.tag == tag, | ||||||
|  |         models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, | ||||||
|  |         models.OutboxObject.is_deleted.is_(False), | ||||||
|  |     ] | ||||||
|  |     tagged_count = await db_session.scalar( | ||||||
|  |         select(func.count(models.OutboxObject.id)) | ||||||
|  |         .join(models.TaggedOutboxObject) | ||||||
|  |         .where(*where) | ||||||
|  |     ) | ||||||
|  |     if not tagged_count: | ||||||
|  |         raise HTTPException(status_code=404) | ||||||
|  |  | ||||||
|  |     outbox_objects = await db_session.execute( | ||||||
|  |         select(models.OutboxObject.ap_id) | ||||||
|  |         .join(models.TaggedOutboxObject) | ||||||
|  |         .where(*where) | ||||||
|  |         .order_by(models.OutboxObject.ap_published_at.desc()) | ||||||
|  |         .limit(20) | ||||||
|  |     ) | ||||||
|     # TODO(ts): implement HTML version |     # TODO(ts): implement HTML version | ||||||
|     # if is_activitypub_requested(request): |     # if is_activitypub_requested(request): | ||||||
|     return ActivityPubResponse( |     return ActivityPubResponse( | ||||||
|         { |         { | ||||||
|             "@context": ap.AS_CTX,  # XXX: extended ctx? |             "@context": ap.AS_CTX, | ||||||
|             "id": BASE_URL + f"/t/{tag}", |             "id": BASE_URL + f"/t/{tag}", | ||||||
|             "type": "OrderedCollection", |             "type": "OrderedCollection", | ||||||
|             "totalItems": 0, |             "totalItems": tagged_count, | ||||||
|             "orderedItems": [], |             "orderedItems": [outbox_object.ap_id for outbox_object in outbox_objects], | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -876,7 +896,8 @@ async def robots_file(): | |||||||
|     return """User-agent: * |     return """User-agent: * | ||||||
| Disallow: /followers | Disallow: /followers | ||||||
| Disallow: /following | Disallow: /following | ||||||
| Disallow: /admin""" | Disallow: /admin | ||||||
|  | Disallow: /remote_follow""" | ||||||
|  |  | ||||||
|  |  | ||||||
| async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]: | async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]: | ||||||
|   | |||||||
| @@ -1,20 +1,92 @@ | |||||||
|  | $font-stack: Helvetica, sans-serif; | ||||||
|  | $background: #002B36;  // solarized background color     | ||||||
|  | // font-family: Inconsolata, monospace; | ||||||
|  | $primary-color: #e14eea; | ||||||
|  | $secondary-color: #32cd32; | ||||||
|  | $muted-color: #586e75; // solarized comment text | ||||||
|  | // #93a1a1; solarized body text | ||||||
|  |  | ||||||
|  |  | ||||||
| body { | body { | ||||||
|     margin: 0; |   font-family: $font-stack; | ||||||
|     padding: 0; |   font-size: 20px; | ||||||
|     display: flex; |   line-height: 32px; | ||||||
|     min-height: 100vh; |   background: $background; | ||||||
|     flex-direction: column; |   color: #ccc; | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  |   display: flex; | ||||||
|  |   min-height: 100vh; | ||||||
|  |   flex-direction: column; | ||||||
|  | } | ||||||
|  | a { | ||||||
|  |   text-decoration: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .activity-main { | ||||||
|  |   a { | ||||||
|  |     color: #ddd; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .activity-wrap { | ||||||
|  | } | ||||||
|  | code, pre { | ||||||
|  |   color: #859900; // #cb4b16; // #268bd2; // #2aa198; | ||||||
|  |   font-family: 'Inconsolata, monospace'; | ||||||
|  | } | ||||||
|  | header { | ||||||
|  |   .title { | ||||||
|  |     font-size: 1.3em; | ||||||
|  |     text-decoration: none; | ||||||
|  |     .handle { | ||||||
|  |       font-size: 0.85em; | ||||||
|  |       color: #93a1a1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   .counter { | ||||||
|  |     color: #93a1a1; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .purple { | ||||||
|  |   color: #e14eea; | ||||||
|  | } | ||||||
|  | a { | ||||||
|  |   color: #e14eea; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .green, a:hover { | ||||||
|  |   color: #32cd32; | ||||||
| } | } | ||||||
| #main { | #main { | ||||||
|     flex: 1;  |     flex: 1;  | ||||||
| } | } | ||||||
| main { | main { | ||||||
|     max-width: 800px; |     width: 100%; | ||||||
|     margin: 20px auto; |     max-width: 960px; | ||||||
|  |     margin: 30px auto; | ||||||
| } | } | ||||||
| footer { | footer { | ||||||
|     max-width: 800px; |     width: 100%; | ||||||
|  |     max-width: 960px; | ||||||
|     margin: 20px auto; |     margin: 20px auto; | ||||||
|  |     color: #93a1a1; | ||||||
|  | } | ||||||
|  | .actor-box { | ||||||
|  |   display: flex; | ||||||
|  |   column-gap: 20px; | ||||||
|  |   margin:20px 0 10px 0; | ||||||
|  |   .icon-box { | ||||||
|  |     flex: 0 0 50px; | ||||||
|  |   } | ||||||
|  |   .actor-handle { | ||||||
|  |     font-size: 0.85em; | ||||||
|  |     line-height: 1em; | ||||||
|  |     color: #93a1a1; | ||||||
|  |   } | ||||||
|  |   .actor-icon { | ||||||
|  |     margin-top: 5px; | ||||||
|  |     max-width: 50px; | ||||||
|  |   } | ||||||
| } | } | ||||||
| #notifications, #followers, #following { | #notifications, #followers, #following { | ||||||
|     ul { |     ul { | ||||||
| @@ -26,47 +98,47 @@ footer { | |||||||
|         display: inline-block; |         display: inline-block; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| .actor-box { |  | ||||||
|     a { | #admin { | ||||||
|         text-decoration: none; | } | ||||||
|  |  | ||||||
|  | .activity-bar { | ||||||
|  | input[type=submit], button { | ||||||
|  |   font-size: 20px; | ||||||
|  |   line-height: 32px; | ||||||
|  |   font-family: "Inconsolata, monospace"; | ||||||
|  |   background: none!important; | ||||||
|  |   border: none; | ||||||
|  |   padding: 0!important; | ||||||
|  |   cursor: pointer; | ||||||
|  |   color: #e14eea; | ||||||
|  |     } | ||||||
|  |     input[type=submit]:hover, button:hover { | ||||||
|  |       color: #32cd32; | ||||||
|  |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #admin { |  | ||||||
| .navbar { |  | ||||||
|   display: grid; |  | ||||||
|   grid-template-rows: auto; |  | ||||||
|   grid-template-columns: 1fr; |  | ||||||
|   grid-auto-flow: dense; |  | ||||||
|   justify-items: stretch; |  | ||||||
|   align-items: stretch; |  | ||||||
|   column-gap: 20px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .logo { |  | ||||||
|   grid-column:-3; |  | ||||||
|   padding: 5px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .menus { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   justify-content: start; |  | ||||||
|   grid-column: 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .menus * { |  | ||||||
|   padding: 5px; |  | ||||||
| } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| nav.flexbox { | nav.flexbox { | ||||||
|     display: flex; |  | ||||||
|     justify-content: space-between; |     input[type=submit] { | ||||||
|     align-items: center; |   font-size: 20px; | ||||||
|  |   line-height: 32px; | ||||||
|  |   font-family: "Inconsolata, monospace"; | ||||||
|  |   background: none!important; | ||||||
|  |   border: none; | ||||||
|  |   padding: 0!important; | ||||||
|  |   cursor: pointer; | ||||||
|  |   color: #e14eea; | ||||||
|  |     } | ||||||
|  |     input[type=submit]:hover { | ||||||
|  |       color: #32cd32; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|     ul {         |     ul {         | ||||||
|         display: flex; |         display: flex; | ||||||
|  |         flex-wrap: wrap; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         list-style-type: none; |         list-style-type: none; | ||||||
|         margin: 0; |         margin: 0; | ||||||
| @@ -81,12 +153,13 @@ nav.flexbox { | |||||||
|             margin-right: 0px; |             margin-right: 0px; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } |     a { | ||||||
| #admin { |  | ||||||
|     a.active { |  | ||||||
|         font-weight: bold; |  | ||||||
|         text-decoration: none; |         text-decoration: none; | ||||||
|     } |     } | ||||||
|  |     a.active { | ||||||
|  |         color: #32cd32; | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -94,12 +167,11 @@ nav.flexbox { | |||||||
|   margin: 0 auto; |   margin: 0 auto; | ||||||
|   padding: 30px 0; |   padding: 30px 0; | ||||||
|   .actor-icon { |   .actor-icon { | ||||||
|     width:48px; |     width: 50px; | ||||||
|     margin-right: 15px; |     margin-right: 15px; | ||||||
|     img { |     margin-top: 5px; | ||||||
|       max-width: 48px; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .activity-content { |   .activity-content { | ||||||
|     display: flex; |     display: flex; | ||||||
|     align-items:flex-start; |     align-items:flex-start; | ||||||
| @@ -112,6 +184,9 @@ nav.flexbox { | |||||||
|         font-weight:normal; |         font-weight:normal; | ||||||
|         margin-left: 5px; |         margin-left: 5px; | ||||||
|       } |       } | ||||||
|  |       .actor-handle { | ||||||
|  |         color: #93a1a1; | ||||||
|  |       } | ||||||
|       .activity-date { float:right; } |       .activity-date { float:right; } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ | |||||||
|     </select> |     </select> | ||||||
|     </p> |     </p> | ||||||
|     {% for emoji in emojis %} |     {% for emoji in emojis %} | ||||||
|     <span class="ji">{{ emoji | emojify | safe }}</span> |     <span class="ji">{{ emoji | emojify(True) | safe }}</span> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     {% for emoji in custom_emojis %} |     {% for emoji in custom_emojis %} | ||||||
|     <span class="ji"><img src="{{ emoji.icon.url }}" alt="{{ emoji.name }}" title="{{ emoji.name }}" class="custom-emoji"></span> |     <span class="ji"><img src="{{ emoji.icon.url }}" alt="{{ emoji.name }}" title="{{ emoji.name }}" class="custom-emoji"></span> | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ | |||||||
| <div class="h-card p-author"> | <div class="h-card p-author"> | ||||||
| <data class="u-photo" value="{{ local_actor.icon_url }}"></data> | <data class="u-photo" value="{{ local_actor.icon_url }}"></data> | ||||||
| <a href="{{ local_actor.url }}" class="u-url u-uid no-hover title"> | <a href="{{ local_actor.url }}" class="u-url u-uid no-hover title"> | ||||||
| 	<span style="font-size:1.1em;">{{ local_actor.name }}</span> | 	<span class="name">{{ local_actor.name }}</span> | ||||||
| 	<span  style="font-size:0.85em;" class="p-name subtitle-username">{{ local_actor.handle }}</span> | 	<span class="p-name handle">{{ local_actor.handle }}</span> | ||||||
| </a> | </a> | ||||||
|  |  | ||||||
| <div class="p-note summary"> | <div class="p-note summary"> | ||||||
| @@ -22,8 +22,8 @@ | |||||||
| <nav class="flexbox"> | <nav class="flexbox"> | ||||||
|     <ul> |     <ul> | ||||||
|         <li>{{ header_link("index", "Notes") }}</li> |         <li>{{ header_link("index", "Notes") }}</li> | ||||||
|         <li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li> |         <li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li> | ||||||
|         <li>{{ header_link("following", "Following") }} <span>{{ 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> | ||||||
|     </ul> |     </ul> | ||||||
| </nav> | </nav> | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| {% extends "layout.html" %} | {% extends "layout.html" %} | ||||||
|  |  | ||||||
| {% block head %} | {% block head %} | ||||||
|  | {% if outbox_object %} | ||||||
| <link rel="alternate" href="{{ request.url }}" type="application/activity+json"> | <link rel="alternate" href="{{ request.url }}" type="application/activity+json"> | ||||||
| <meta name="description" content="{{ outbox_object.content | html2text | trim | truncate(50) }}"> | <meta name="description" content="{{ outbox_object.content | html2text | trim | truncate(50) }}"> | ||||||
| <meta content="article" property="og:type" /> | <meta content="article" property="og:type" /> | ||||||
| @@ -11,6 +12,7 @@ | |||||||
| <meta content="{{ outbox_object.content | html2text | trim | truncate(50) }}" property="og:description" /> | <meta content="{{ outbox_object.content | html2text | trim | truncate(50) }}" 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" /> | ||||||
|  | {% endif %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
|   | |||||||
| @@ -136,13 +136,13 @@ | |||||||
|  |  | ||||||
| {% macro display_actor(actor, actors_metadata) %} | {% macro display_actor(actor, actors_metadata) %} | ||||||
| {% set metadata = actors_metadata.get(actor.ap_id) %} | {% set metadata = actors_metadata.get(actor.ap_id) %} | ||||||
| <div style="display: flex;column-gap: 20px;margin:20px 0 10px 0;" class="actor-box h-card p-author"> | <div class="actor-box h-card p-author"> | ||||||
|     <div style="flex: 0 0 48px;"> |     <div class="icon-box"> | ||||||
|         <img src="{{ actor.resized_icon_url }}" style="max-width:45px;"> |         <img src="{{ actor.resized_icon_url }}" class="actor-icon"> | ||||||
|     </div> |     </div> | ||||||
|     <a href="{{ actor.url }}" class="u-url" style=""> |     <a href="{{ actor.url }}" class="u-url" style=""> | ||||||
|         <div><strong>{{ actor.display_name | clean_html(actor) | safe  }}</strong></div> |         <div><strong>{{ actor.display_name | clean_html(actor) | safe  }}</strong></div> | ||||||
|         <div class="p-name">{{ actor.handle }}</div> |         <div class="actor-handle p-name">{{ actor.handle }}</div> | ||||||
|     </a> |     </a> | ||||||
| </div> | </div> | ||||||
| {% if is_admin and metadata %} | {% if is_admin and metadata %} | ||||||
| @@ -171,14 +171,13 @@ | |||||||
| {% macro display_og_meta(object) %} | {% macro display_og_meta(object) %} | ||||||
| {% if object.og_meta %} | {% if object.og_meta %} | ||||||
| {% for og_meta in object.og_meta %} | {% for og_meta in object.og_meta %} | ||||||
| <div style="display:flex;column-gap: 20px;margin:20px 0;"> | <div class="activity-og-meta" style="display:flex;column-gap: 20px;margin:20px 0;"> | ||||||
|     {% if og_meta.image %} |     {% if og_meta.image %} | ||||||
|     <div> |     <div> | ||||||
|     <img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;"> |     <img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;"> | ||||||
|     </div> |     </div> | ||||||
|     <div> |     <div> | ||||||
|     <a href="{{ og_meta.url }}">{{ og_meta.title }}</a> |     <a href="{{ og_meta.url }}">{{ og_meta.title }}</a> | ||||||
|     {% if og_meta.description %}<p>{{ og_meta.description }}</p>{% endif %} |  | ||||||
|     <small style="display:block;">{{ og_meta.site_name }}</small> |     <small style="display:block;">{{ og_meta.site_name }}</small> | ||||||
|     </div> |     </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| @@ -279,7 +278,7 @@ | |||||||
|       <img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon"> |       <img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon"> | ||||||
|     <div class="activity-header"> |     <div class="activity-header"> | ||||||
|         <strong>{{ object.actor.display_name }}</strong> |         <strong>{{ object.actor.display_name }}</strong> | ||||||
|         <a href="{{ object.actor.url}}" class="p-author h-card">{{ object.actor.handle }}</a> |         <a href="{{ object.actor.url}}" class="actor-handle p-author h-card">{{ object.actor.handle }}</a> | ||||||
|         <span class="activity-date" title="{{ object.ap_published_at.isoformat() }}"> |         <span class="activity-date" title="{{ object.ap_published_at.isoformat() }}"> | ||||||
|             {{ object.visibility.value }} |             {{ object.visibility.value }} | ||||||
|             <a href="{{ object.url }}" class="u-url u-uid"><time class="dt-published" datetime="{{ object.ap_published_at }}">{{ object.ap_published_at | timeago }}</time></a> |             <a href="{{ object.url }}" class="u-url u-uid"><time class="dt-published" datetime="{{ object.ap_published_at }}">{{ object.ap_published_at | timeago }}</time></a> | ||||||
|   | |||||||
| @@ -16,6 +16,19 @@ from tests import factories | |||||||
| from tests.utils import generate_admin_session_cookies | from tests.utils import generate_admin_session_cookies | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_outbox__no_activities( | ||||||
|  |     db: Session, | ||||||
|  |     client: TestClient, | ||||||
|  | ) -> None: | ||||||
|  |     response = client.get("/outbox", headers={"Accept": ap.AP_CONTENT_TYPE}) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     json_response = response.json() | ||||||
|  |     assert json_response["totalItems"] == 0 | ||||||
|  |     assert json_response["orderedItems"] == [] | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_send_follow_request( | def test_send_follow_request( | ||||||
|     db: Session, |     db: Session, | ||||||
|     client: TestClient, |     client: TestClient, | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								tests/test_tags.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/test_tags.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | from fastapi.testclient import TestClient | ||||||
|  | from sqlalchemy.orm import Session | ||||||
|  |  | ||||||
|  | from app import activitypub as ap | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_tags__no_tags( | ||||||
|  |     db: Session, | ||||||
|  |     client: TestClient, | ||||||
|  | ) -> None: | ||||||
|  |     response = client.get("/t/nope", headers={"Accept": ap.AP_CONTENT_TYPE}) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 404 | ||||||
		Reference in New Issue
	
	Block a user