mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2025-06-05 21:59:23 +02:00
Compare commits
15 Commits
2.0.0-rc.1
...
2.0.0-rc.1
Author | SHA1 | Date | |
---|---|---|---|
ce6f9238f3 | |||
3f129855d1 | |||
3fc567861b | |||
7b784e3011 | |||
5d1ae0c9cd | |||
88dd2443d7 | |||
4045902068 | |||
20109b45da | |||
94d14fbef3 | |||
f34e0b376b | |||
51c596dd1d | |||
dfc7ab0470 | |||
5d35d5c0a0 | |||
17921c1097 | |||
24147aedef |
@ -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" \
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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"}
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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(") -> ", ") -> ")
|
code_content = code_content.replace(") -> ", ") -> ")
|
||||||
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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
Reference in New Issue
Block a user