1
0
mirror of https://git.sr.ht/~tsileo/microblog.pub synced 2025-06-05 21:59:23 +02:00

15 Commits

13 changed files with 105 additions and 41 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.10-slim as python-base FROM python:3.11-slim as python-base
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
POETRY_HOME="/opt/poetry" \ POETRY_HOME="/opt/poetry" \

View File

@ -10,6 +10,7 @@ Instances in the wild:
- [microblog.pub](https://microblog.pub/) (follow to get updated about the project) - [microblog.pub](https://microblog.pub/) (follow to get updated about the project)
- [hexa.ninja](https://hexa.ninja) (theme customization example) - [hexa.ninja](https://hexa.ninja) (theme customization example)
- [testing.microblog.pub](https://testing.microblog.pub/) - [testing.microblog.pub](https://testing.microblog.pub/)
- [Irish Left Archive](https://posts.leftarchive.ie/) (another theme customization example)
There are still some rough edges, but the server is mostly functional. There are still some rough edges, but the server is mostly functional.

View File

@ -189,8 +189,11 @@ async def admin_new(
content += f"{in_reply_to_object.actor.handle} " content += f"{in_reply_to_object.actor.handle} "
for tag in in_reply_to_object.tags: for tag in in_reply_to_object.tags:
if tag.get("type") == "Mention" and tag["name"] != LOCAL_ACTOR.handle: if tag.get("type") == "Mention" and tag["name"] != LOCAL_ACTOR.handle:
mentioned_actor = await fetch_actor(db_session, tag["href"]) try:
content += f"{mentioned_actor.handle} " mentioned_actor = await fetch_actor(db_session, tag["href"])
content += f"{mentioned_actor.handle} "
except Exception:
logger.exception(f"Failed to lookup {mentioned_actor}")
# Copy the content warning if any # Copy the content warning if any
if in_reply_to_object.summary: if in_reply_to_object.summary:

View File

@ -23,6 +23,13 @@ requests_loader = pyld.documentloader.requests.requests_document_loader()
def _loader(url, options={}): def _loader(url, options={}):
# See https://github.com/digitalbazaar/pyld/issues/133 # See https://github.com/digitalbazaar/pyld/issues/133
options["headers"]["Accept"] = "application/ld+json" options["headers"]["Accept"] = "application/ld+json"
# XXX: temp fix/hack is it seems to be down for now
if url == "https://w3id.org/identity/v1":
url = (
"https://raw.githubusercontent.com/web-payments/web-payments.org"
"/master/contexts/identity-v1.jsonld"
)
return requests_loader(url, options) return requests_loader(url, options)
@ -34,7 +41,7 @@ def _options_hash(doc: ap.RawObject) -> str:
for k in ["type", "id", "signatureValue"]: for k in ["type", "id", "signatureValue"]:
if k in doc: if k in doc:
del doc[k] del doc[k]
doc["@context"] = "https://w3id.org/identity/v1" doc["@context"] = "https://w3id.org/security/v1"
normalized = jsonld.normalize( normalized = jsonld.normalize(
doc, {"algorithm": "URDNA2015", "format": "application/nquads"} doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
) )

View File

@ -1256,7 +1256,11 @@ async def post_remote_interaction(
@app.get("/.well-known/webfinger") @app.get("/.well-known/webfinger")
async def wellknown_webfinger(resource: str) -> JSONResponse: async def wellknown_webfinger(resource: str) -> JSONResponse:
"""Exposes/servers WebFinger data.""" """Exposes/servers WebFinger data."""
if resource not in [f"acct:{USERNAME}@{DOMAIN}", ID]: if resource not in [
f"acct:{USERNAME}@{WEBFINGER_DOMAIN}",
ID,
f"acct:{USERNAME}@{DOMAIN}",
]:
logger.info(f"Got invalid req for {resource}") logger.info(f"Got invalid req for {resource}")
raise HTTPException(status_code=404) raise HTTPException(status_code=404)

View File

@ -432,8 +432,7 @@ a.label-btn {
.activity-attachment { .activity-attachment {
margin: 30px 0 20px 0; margin: 30px 0 20px 0;
img, audio, video { img, audio, video {
width: 100%; max-width: calc(min(740px, 100%));
max-width: 740px;
} }
} }
img.inline-img { img.inline-img {

View File

@ -3,12 +3,12 @@ import typing
from loguru import logger from loguru import logger
from mistletoe import Document # type: ignore from mistletoe import Document # type: ignore
from mistletoe.block_token import CodeFence # type: ignore
from mistletoe.html_renderer import HTMLRenderer # type: ignore from mistletoe.html_renderer import HTMLRenderer # type: ignore
from mistletoe.span_token import SpanToken # type: ignore from mistletoe.span_token import SpanToken # type: ignore
from pygments import highlight # type: ignore
from pygments.formatters import HtmlFormatter # type: ignore from pygments.formatters import HtmlFormatter # type: ignore
from pygments.lexers import get_lexer_by_name as get_lexer # type: ignore from pygments.lexers import get_lexer_by_name as get_lexer # type: ignore
from pygments.lexers import guess_lexer # type: ignore from pygments.util import ClassNotFound # type: ignore
from sqlalchemy import select from sqlalchemy import select
from app import webfinger from app import webfinger
@ -104,10 +104,16 @@ class CustomRenderer(HTMLRenderer):
) )
return link return link
def render_block_code(self, token: typing.Any) -> str: def render_block_code(self, token: CodeFence) -> str:
lexer_attr = ""
try:
lexer = get_lexer(token.language)
lexer_attr = f' data-microblogpub-lexer="{lexer.aliases[0]}"'
except ClassNotFound:
pass
code = token.children[0].content code = token.children[0].content
lexer = get_lexer(token.language) if token.language else guess_lexer(code) return f"<pre><code{lexer_attr}>\n{code}\n</code></pre>"
return highlight(code, lexer, _FORMATTER)
async def _prefetch_mentioned_actors( async def _prefetch_mentioned_actors(

View File

@ -11,8 +11,8 @@
<ul class="h-feed" id="articles"> <ul class="h-feed" id="articles">
<data class="p-name" value="{{ local_actor.display_name}}'s articles"></data> <data class="p-name" value="{{ local_actor.display_name}}'s articles"></data>
{% for outbox_object in objects %} {% for outbox_object in objects %}
<li> <li class="h-entry">
<span class="muted">{{ outbox_object.ap_published_at.strftime("%b %d, %Y") }}</span> <a href="{{ outbox_object.url }}">{{ outbox_object.name }}</a> <time class="muted dt-published" datetime="{{ outbox_object.ap_published_at.isoformat() }}">{{ outbox_object.ap_published_at.strftime("%b %d, %Y") }}</time> <a href="{{ outbox_object.url }}" class="u-url u-uid p-name">{{ outbox_object.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -1,5 +1,6 @@
import datetime import datetime
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timezone
from typing import Any from typing import Any
from typing import Optional from typing import Optional
@ -9,7 +10,7 @@ from app import media
from app.models import InboxObject from app.models import InboxObject
from app.models import Webmention from app.models import Webmention
from app.utils.datetime import parse_isoformat from app.utils.datetime import parse_isoformat
from app.utils.url import make_abs from app.utils.url import must_make_abs
@dataclass @dataclass
@ -39,13 +40,15 @@ class Face:
return cls( return cls(
ap_actor_id=None, ap_actor_id=None,
url=( url=(
item["properties"]["url"][0] must_make_abs(
item["properties"]["url"][0], webmention.source
)
if item["properties"].get("url") if item["properties"].get("url")
else webmention.source else webmention.source
), ),
name=item["properties"]["name"][0], name=item["properties"]["name"][0],
picture_url=media.resized_media_url( picture_url=media.resized_media_url(
make_abs( must_make_abs(
item["properties"]["photo"][0], webmention.source item["properties"]["photo"][0], webmention.source
), # type: ignore ), # type: ignore
50, 50,
@ -65,7 +68,7 @@ class Face:
url=webmention.source, url=webmention.source,
name=author["properties"]["name"][0], name=author["properties"]["name"][0],
picture_url=media.resized_media_url( picture_url=media.resized_media_url(
make_abs( must_make_abs(
author["properties"]["photo"][0], webmention.source author["properties"]["photo"][0], webmention.source
), # type: ignore ), # type: ignore
50, 50,
@ -96,13 +99,13 @@ def _parse_face(webmention: Webmention, items: list[dict[str, Any]]) -> Face | N
return Face( return Face(
ap_actor_id=None, ap_actor_id=None,
url=( url=(
item["properties"]["url"][0] must_make_abs(item["properties"]["url"][0], webmention.source)
if item["properties"].get("url") if item["properties"].get("url")
else webmention.source else webmention.source
), ),
name=item["properties"]["name"][0], name=item["properties"]["name"][0],
picture_url=media.resized_media_url( picture_url=media.resized_media_url(
make_abs( must_make_abs(
item["properties"]["photo"][0], webmention.source item["properties"]["photo"][0], webmention.source
), # type: ignore ), # type: ignore
50, 50,
@ -140,13 +143,23 @@ class WebmentionReply:
f"webmention id={webmention.id}" f"webmention id={webmention.id}"
) )
break break
if "published" in item["properties"]:
published_at = (
parse_isoformat(item["properties"]["published"][0])
.astimezone(timezone.utc)
.replace(tzinfo=None)
)
else:
published_at = webmention.created_at # type: ignore
return cls( return cls(
face=face, face=face,
content=item["properties"]["content"][0]["html"], content=item["properties"]["content"][0]["html"],
url=item["properties"]["url"][0], url=must_make_abs(
published_at=parse_isoformat( item["properties"]["url"][0], webmention.source
item["properties"]["published"][0] ),
).replace(tzinfo=None), published_at=published_at,
in_reply_to=webmention.target, # type: ignore in_reply_to=webmention.target, # type: ignore
webmention_id=webmention.id, # type: ignore webmention_id=webmention.id, # type: ignore
) )

View File

@ -32,23 +32,22 @@ def highlight(html: str) -> str:
# If this comes from a microblog.pub instance we may have the language # If this comes from a microblog.pub instance we may have the language
# in the class name # in the class name
if "class" in code.attrs and code.attrs["class"][0].startswith("language-"): if "data-microblogpub-lexer" in code.attrs:
try: try:
lexer = get_lexer_by_name( lexer = get_lexer_by_name(code.attrs["data-microblogpub-lexer"])
code.attrs["class"][0].removeprefix("language-")
)
except Exception: except Exception:
lexer = guess_lexer(code_content) lexer = guess_lexer(code_content)
else:
lexer = guess_lexer(code_content)
# Replace the code with Pygment output # Replace the code with Pygment output
# XXX: the HTML escaping causes issue with Python type annotations # XXX: the HTML escaping causes issue with Python type annotations
code_content = code_content.replace(") -&gt; ", ") -> ") code_content = code_content.replace(") -&gt; ", ") -> ")
code.parent.replaceWith( code.parent.replaceWith(
BeautifulSoup( BeautifulSoup(
phighlight(code_content, lexer, _FORMATTER), "html5lib" phighlight(code_content, lexer, _FORMATTER), "html5lib"
).body.next ).body.next
) )
else:
code.name = "div"
code["class"] = code.get("class", []) + ["highlight"]
return soup.body.encode_contents().decode() return soup.body.encode_contents().decode()

View File

@ -21,6 +21,13 @@ def make_abs(url: str | None, parent: str) -> str | None:
) )
def must_make_abs(url: str | None, parent: str) -> str:
abs_url = make_abs(url, parent)
if not abs_url:
raise ValueError("missing URL")
return abs_url
class InvalidURLError(Exception): class InvalidURLError(Exception):
pass pass

View File

@ -191,6 +191,29 @@ http {
} }
``` ```
## (Advanced) Running on a subdomain
It is possible to run microblogpub on a subdomain (`sub.domain.tld`) while being reachable from the root root domain (`domain.tld`) using the `name@domain.tld` handle.
This requires forwarding/proxying requests from the root domain to the subdomain, for example using NGINX:
```nginx
location /.well-known/webfinger {
add_header Access-Control-Allow-Origin '*';
return 301 https://sub.domain.tld$request_uri;
}
```
And updating `data/profile.toml` to specify the root domain as the webfinger domain:
```toml
webfinger_domain = "domain.tld"
```
Once configured correctly, people will be able to follow you using `name@domain.tld`, while using `sub.domain.tld` for the web interface.
## (Advanced) Running from subpath ## (Advanced) Running from subpath
It is possible to configure microblogpub to run from subpath. It is possible to configure microblogpub to run from subpath.

View File

@ -320,7 +320,7 @@ First you need to grab the "ActivityPub actor URL" for your existing account:
```bash ```bash
# For a Python install # For a Python install
poetry run inv webfinger username@domain.tld poetry run inv webfinger username@instance-you-want-to-move-from.tld
``` ```
Edit the config. Edit the config.
@ -329,7 +329,7 @@ Edit the config.
```bash ```bash
# For a Docker install # For a Docker install
make account=username@domain.tld webfinger make account=username@instance-you-want-to-move-from.tld webfinger
``` ```
Edit the config. Edit the config.
@ -339,11 +339,13 @@ Edit the config.
And add a reference to your old/existing account in `profile.toml`: And add a reference to your old/existing account in `profile.toml`:
```toml ```toml
also_known_as = "my@old-account.com" also_known_as = "https://instance-you-want-to-move-form.tld/users/username"
``` ```
Restart the server, and you should be able to complete the move from your existing account. Restart the server, and you should be able to complete the move from your existing account.
Note that if you already have a redirect in place on Mastodon, you may have to remove it before initiating the migration.
## Import follows from Mastodon ## Import follows from Mastodon
You can import the list of follows/following accounts from Mastodon. You can import the list of follows/following accounts from Mastodon.