forked from forks/microblog.pub
Tweak design and AP tag supports
This commit is contained in:
parent
9a4643fa3e
commit
d164d6d2dd
8 changed files with 189 additions and 66 deletions
29
app/main.py
29
app/main.py
|
@ -551,15 +551,35 @@ async def tag_by_name(
|
|||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> 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
|
||||
# if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(
|
||||
{
|
||||
"@context": ap.AS_CTX, # XXX: extended ctx?
|
||||
"@context": ap.AS_CTX,
|
||||
"id": BASE_URL + f"/t/{tag}",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 0,
|
||||
"orderedItems": [],
|
||||
"totalItems": tagged_count,
|
||||
"orderedItems": [outbox_object.ap_id for outbox_object in outbox_objects],
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -876,7 +896,8 @@ async def robots_file():
|
|||
return """User-agent: *
|
||||
Disallow: /followers
|
||||
Disallow: /following
|
||||
Disallow: /admin"""
|
||||
Disallow: /admin
|
||||
Disallow: /remote_follow"""
|
||||
|
||||
|
||||
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 {
|
||||
font-family: $font-stack;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
background: $background;
|
||||
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 {
|
||||
flex: 1;
|
||||
}
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 30px auto;
|
||||
}
|
||||
footer {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
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 {
|
||||
ul {
|
||||
|
@ -26,47 +98,47 @@ footer {
|
|||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.actor-box {
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
#admin {
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
input[type=submit] {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
|
@ -81,12 +153,13 @@ nav.flexbox {
|
|||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
#admin {
|
||||
a.active {
|
||||
font-weight: bold;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.active {
|
||||
color: #32cd32;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -94,12 +167,11 @@ nav.flexbox {
|
|||
margin: 0 auto;
|
||||
padding: 30px 0;
|
||||
.actor-icon {
|
||||
width:48px;
|
||||
width: 50px;
|
||||
margin-right: 15px;
|
||||
img {
|
||||
max-width: 48px;
|
||||
}
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
display: flex;
|
||||
align-items:flex-start;
|
||||
|
@ -112,6 +184,9 @@ nav.flexbox {
|
|||
font-weight:normal;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.actor-handle {
|
||||
color: #93a1a1;
|
||||
}
|
||||
.activity-date { float:right; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</select>
|
||||
</p>
|
||||
{% for emoji in emojis %}
|
||||
<span class="ji">{{ emoji | emojify | safe }}</span>
|
||||
<span class="ji">{{ emoji | emojify(True) | safe }}</span>
|
||||
{% endfor %}
|
||||
{% for emoji in custom_emojis %}
|
||||
<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">
|
||||
<data class="u-photo" value="{{ local_actor.icon_url }}"></data>
|
||||
<a href="{{ local_actor.url }}" class="u-url u-uid no-hover title">
|
||||
<span style="font-size:1.1em;">{{ local_actor.name }}</span>
|
||||
<span style="font-size:0.85em;" class="p-name subtitle-username">{{ local_actor.handle }}</span>
|
||||
<span class="name">{{ local_actor.name }}</span>
|
||||
<span class="p-name handle">{{ local_actor.handle }}</span>
|
||||
</a>
|
||||
|
||||
<div class="p-note summary">
|
||||
|
@ -22,8 +22,8 @@
|
|||
<nav class="flexbox">
|
||||
<ul>
|
||||
<li>{{ header_link("index", "Notes") }}</li>
|
||||
<li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li>
|
||||
<li>{{ header_link("following", "Following") }} <span>{{ following_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("get_remote_follow", "Remote follow") }}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
{% if outbox_object %}
|
||||
<link rel="alternate" href="{{ request.url }}" type="application/activity+json">
|
||||
<meta name="description" content="{{ outbox_object.content | html2text | trim | truncate(50) }}">
|
||||
<meta content="article" property="og:type" />
|
||||
|
@ -11,6 +12,7 @@
|
|||
<meta content="{{ outbox_object.content | html2text | trim | truncate(50) }}" property="og:description" />
|
||||
<meta content="{{ local_actor.icon_url }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -136,13 +136,13 @@
|
|||
|
||||
{% macro display_actor(actor, actors_metadata) %}
|
||||
{% 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 style="flex: 0 0 48px;">
|
||||
<img src="{{ actor.resized_icon_url }}" style="max-width:45px;">
|
||||
<div class="actor-box h-card p-author">
|
||||
<div class="icon-box">
|
||||
<img src="{{ actor.resized_icon_url }}" class="actor-icon">
|
||||
</div>
|
||||
<a href="{{ actor.url }}" class="u-url" style="">
|
||||
<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>
|
||||
</div>
|
||||
{% if is_admin and metadata %}
|
||||
|
@ -171,14 +171,13 @@
|
|||
{% macro display_og_meta(object) %}
|
||||
{% if 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 %}
|
||||
<div>
|
||||
<img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;">
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -279,7 +278,7 @@
|
|||
<img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon">
|
||||
<div class="activity-header">
|
||||
<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() }}">
|
||||
{{ 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>
|
||||
|
|
|
@ -16,6 +16,19 @@ from tests import factories
|
|||
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(
|
||||
db: Session,
|
||||
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
|
Loading…
Reference in a new issue