Tweak design and AP tag supports
parent
9a4643fa3e
commit
d164d6d2dd
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 {
|
||||||
|
font-family: $font-stack;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 32px;
|
||||||
|
background: $background;
|
||||||
|
color: #ccc;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
flex-direction: column;
|
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,
|
||||||
|
|
|
@ -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 New Issue